npm package: @studiometa/forge-mcp
Description: MCP server for Laravel Forge — Model Context Protocol integration for AI agents
Author: Studio Meta
Latest version: 0.4.3

# @studiometa/forge-mcp

[![npm version](https://img.shields.io/npm/v/@studiometa/forge-mcp?style=flat&colorB=3e63dd&colorA=414853)](https://www.npmjs.com/package/@studiometa/forge-mcp)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat&colorB=3e63dd&colorA=414853)](https://opensource.org/licenses/MIT)

MCP (Model Context Protocol) server for [Laravel Forge](https://forge.laravel.com). Enables Claude Desktop and other MCP clients to manage servers, sites, deployments, and more through natural language.

## Features

- **Two tools with clear safety split** — `forge` (read) and `forge_write` (write)
- MCP clients auto-approve `forge` reads, always prompt for `forge_write` writes
- Resource/action routing from centralized constants
- Built-in help system — `action=help` for any resource
- Stdio and Streamable HTTP transports
- Configuration tools for interactive token setup

## Installation

```bash
npm install -g @studiometa/forge-mcp
```

## Claude Desktop Configuration

Add to your Claude Desktop config:

- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
- **Linux**: `~/.config/Claude/claude_desktop_config.json`

```json
{
  "mcpServers": {
    "forge": {
      "command": "forge-mcp",
      "env": {
        "FORGE_API_TOKEN": "your-api-token",
        "FORGE_ORG": "your-organization-slug"
      }
    }
  }
}
```

Alternatively, omit the `env` block and ask Claude to configure credentials using the `forge_configure` tool (accepts `apiToken` and `organizationSlug` parameters).

### Read-Only Mode

To guarantee no write operations are possible at the server level:

```json
{
  "mcpServers": {
    "forge": {
      "command": "forge-mcp",
      "args": ["--read-only"],
      "env": {
        "FORGE_API_TOKEN": "your-api-token"
      }
    }
  }
}
```

Or via environment variable: `FORGE_READ_ONLY=true`.

When enabled, the `forge_write` tool is not registered at all — only `forge`, `forge_configure`, and `forge_get_config` are available.

## Tools

### `forge` — Read Operations

Safe, read-only queries. Annotated `readOnlyHint: true` so MCP clients can auto-approve.

**Actions**: `list`, `get`, `resolve`, `context`, `help`, `schema`

```json
{ "resource": "servers", "action": "list" }
{ "resource": "servers", "action": "get", "id": "123" }
{ "resource": "sites", "action": "list", "server_id": "123" }
{ "resource": "servers", "action": "help" }
{ "resource": "servers", "action": "resolve", "query": "prod" }
{ "resource": "servers", "action": "context", "id": "123" }
{ "resource": "sites", "action": "context", "server_id": "123", "id": "456" }
{ "resource": "batch", "action": "run", "operations": [{ "resource": "servers", "action": "list" }, { "resource": "recipes", "action": "list" }] }
```

### `forge_write` — Write Operations

Mutating operations. Annotated `destructiveHint: true` so MCP clients always prompt for confirmation.

**Actions**: `create`, `update`, `delete`, `deploy`, `reboot`, `restart`, `activate`, `run`

```json
{ "resource": "deployments", "action": "deploy", "server_id": "123", "site_id": "456" }
// deploy blocks until complete, returning: status, deployment log, elapsed time
{ "resource": "servers", "action": "reboot", "id": "123" }
{ "resource": "daemons", "action": "create", "server_id": "123", "command": "php artisan queue:work" }
```

### Resources & Actions

| Resource        | Read Actions                | Write Actions            | Required Fields            |
| --------------- | --------------------------- | ------------------------ | -------------------------- |
| servers         | list, get, resolve, context | create, delete, reboot   | id (for get/delete/reboot) |
| sites           | list, get, resolve, context | create, delete           | server_id                  |
| deployments     | list, get                   | deploy, update           | server_id, site_id         |
| env             | get                         | update                   | server_id, site_id         |
| nginx           | get                         | update                   | server_id, site_id         |
| certificates    | list, get                   | create, delete, activate | server_id, site_id         |
| databases       | list, get                   | create, delete           | server_id                  |
| database-users  | list, get                   | create, delete           | server_id                  |
| daemons         | list, get                   | create, delete, restart  | server_id                  |
| firewall-rules  | list, get                   | create, delete           | server_id                  |
| ssh-keys        | list, get                   | create, delete           | server_id                  |
| security-rules  | list, get                   | create, delete           | server_id, site_id         |
| redirect-rules  | list, get                   | create, delete           | server_id, site_id         |
| monitors        | list, get                   | create, delete           | server_id                  |
| nginx-templates | list, get                   | create, update, delete   | server_id                  |
| scheduled-jobs  | list, get                   | create, delete           | server_id                  |
| backups         | list, get                   | create, delete           | server_id                  |
| commands        | list, get                   | create                   | server_id, site_id         |
| recipes         | list, get                   | create, delete, run      | id (for get/delete/run)    |
| user            | get                         | —                        | —                          |
| batch           | run                         | —                        | operations array           |

### Auto-Resolve: Names Instead of IDs

The `server_id` and `site_id` fields accept **names** in addition to numeric IDs. When a non-numeric value is provided, it is resolved automatically via partial, case-insensitive match against the list of resources.

- `server_id: "prod"` → resolves to the server whose name contains "prod" (must be unique)
- `site_id: "example"` → resolves to the site whose domain contains "example" (requires `server_id`)

Use `action: "resolve"` explicitly when you want to search before committing to an ID:

```json
{ "resource": "servers", "action": "resolve", "query": "prod" }
{ "resource": "sites", "action": "resolve", "server_id": "123", "query": "example" }
```

Resolution fails (and returns an error) when the query matches zero or more than one resource — in that case, use the numeric ID directly.

### Discovery

Use `action: "help"` with any resource, or `action: "context"` to fetch a resource and all its sub-resources in one call:

```json
{ "resource": "servers", "action": "help" }
{ "resource": "deployments", "action": "help" }
{ "resource": "servers", "action": "context", "id": "123" }
{ "resource": "sites", "action": "context", "server_id": "123", "id": "456" }
```

Use `resource: "batch"` to fan out multiple reads in a single round-trip (max 10 operations):

```json
{
  "resource": "batch",
  "action": "run",
  "operations": [
    { "resource": "servers", "action": "list" },
    { "resource": "sites", "action": "list", "server_id": "123" },
    { "resource": "recipes", "action": "list" }
  ]
}
```

## Stdio-Only Tools

| Tool               | Description                                          |
| ------------------ | ---------------------------------------------------- |
| `forge_configure`  | Save API token and organization slug to local config |
| `forge_get_config` | Show current config (token masked, org slug visible) |

## Audit Logging

All write operations (`forge_write` tool calls) are automatically logged for traceability:

- **Default path**: `~/.config/forge-tools/audit.log`
- **Override**: Set `FORGE_AUDIT_LOG` environment variable
- **Format**: JSON lines (via pino) with timestamp, resource, action, sanitized args, and status
- **Safety**: Logging never interrupts operations — silent on failure

The CLI also logs write commands to the same audit log.

## Getting Your API Token

1. Log into [Laravel Forge](https://forge.laravel.com)
2. Go to **Account → API Tokens**
3. Create a new token with the scopes you need
4. Copy the token (it's only shown once)

## License

MIT © [Studio Meta](https://www.studiometa.fr)