Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
34e93d9
feat(mcp): add Phase 1 foundation — config types, validation, audit c…
May 25, 2026
dc8253b
feat(mcp): add Transport interface, HTTP transport, OAuth 2.1 PKCE flow
May 25, 2026
4b7c3a6
feat(mcp): add JSON-RPC Client + Server lifecycle state machine
May 25, 2026
f69edb7
feat(mcp): add Manager + MCPTool adapter + registry namespacing rule
May 25, 2026
b44c50f
feat(mcp): wire Manager into runner, merge MCP hosts into egress
May 25, 2026
25f351c
feat(mcp): add forge mcp CLI subcommands; remove mcp_call adapter
May 25, 2026
96fbd94
docs(mcp): Phase 1 operator docs + multi-server E2E test
May 25, 2026
2cd6eb3
fix(mcp): state machine silently broken on every failure path (review…
May 25, 2026
44bad6e
fix(mcp/oauth): refresh can hang forever and leak singleflight slot (…
May 25, 2026
2d1785b
fix(mcp/transport): push() silently drops oldest frame, hanging calle…
May 25, 2026
ca1a057
fix(mcp): move os/exec out of forge-core/mcp (review B4)
May 25, 2026
24c17dc
fix(mcp): notification error swallowed + phase classifier brittle (re…
May 25, 2026
51a7b3d
fix(mcp): unknown auth.Type silently disables auth headers (review B6)
May 25, 2026
f4717bb
fix(mcp/transport): Mcp-Session-Id rotation accepted silently (review…
May 25, 2026
4795f07
fix(mcp/oauth): laptop login server lacked timeouts, Shutdown unbound…
May 25, 2026
d671734
fix(mcp/tools): reject empty/__-bearing tool names at three boundarie…
May 25, 2026
c1420f6
refactor(mcp/manager): remove dead drain goroutine + unread errs chan…
May 25, 2026
7123195
fix(mcp): address review nits (B11–B16)
May 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,62 @@
# Changelog

## v0.12.0 — Phase 1: MCP integration (HTTP transport) — in progress

### Added

- **Model Context Protocol (MCP) HTTP client support.** Configure servers
under a new `mcp:` block in `forge.yaml`; discovered tools are
registered as namespaced `<server>__<tool>` first-class tools that
flow through the existing LLM executor.
- **`forge mcp` subcommands:**
- `forge mcp list` — show every configured server, its state, and
the number of tools it exposes after filtering.
- `forge mcp test <name>` — connect, list tools, optionally call one
with `--call <tool> --args '<json>'`.
- `forge mcp login <name>` — laptop-time OAuth 2.1 PKCE flow.
- `forge mcp logout <name>` — remove stored OAuth tokens.
- **OAuth 2.1 PKCE** for hosted MCP servers (Linear, Notion, Atlassian,
GitHub hosted MCP, etc.). Tokens persist via the existing
AES-256-GCM keyring at `~/.forge/credentials/mcp_<name>.json`
(encrypted when `FORGE_PASSPHRASE` is set).
- **Audit events** (NDJSON to stderr, no byte payload ever):
`mcp_server_started`, `mcp_server_failed`, `mcp_server_degraded`,
`mcp_tool_call`, `mcp_tool_result`, `mcp_tool_conflict`,
`mcp_token_refresh`.
- **Egress integration.** MCP server hosts auto-merged into the egress
allowlist (mirroring `auth_domains`) so an HTTP MCP call cannot
silently be blocked at runtime.
- **Tool namespacing.** `tools.Registry.Register` rejects names
containing `__` unless the tool implements the new
`tools.MCPSource` marker interface, preventing builtins from
shadowing MCP-namespaced tools.

### Removed

- **`mcp_call` adapter tool removed.** Superseded by the new `mcp:`
configuration block in `forge.yaml`, which exposes each MCP
server's tools as first-class namespaced tools — strictly better UX
for the LLM than a single meta-tool. See `docs/mcp/index.md` for
the migration path.

### Notes

- **Phase 1 supports HTTP transport only.** Stdio MCP servers (Notion,
Linear community, Atlassian, the modelcontextprotocol/servers
reference set) are on the roadmap. `transport: stdio` is rejected at
`forge validate` time with the message
`"stdio is on the roadmap; Phase 1 supports HTTP transport only"`.
- **MCP protocol version pinned to `2025-06-18`**. Handshake hard-fails
on mismatch — version negotiation is intentionally absent.
- **OAuth callback** runs on a `127.0.0.1` loopback listener; it is a
laptop-time operation. For K8s deployments, run
`forge mcp login <name>` locally, then mount the resulting
credentials file as a Secret and point `MCP_TOKEN_STORE_PATH` at it.
- **No new top-level dependencies** — JSON Schema validation reuses
the existing `xeipuuv/gojsonschema` already in `go.mod`.

---

## v0.11.0 — Phase 2: cloud-native auth providers (in progress)

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ You write a `SKILL.md`. Forge compiles it into a secure, runnable agent with egr
| Cron Scheduling | Recurring tasks with channel delivery |
| Memory | Session persistence + long-term vector search |
| LLM Fallbacks | Multi-provider with automatic failover |
| MCP Client | Connect to any HTTP MCP server (Linear, Notion, Atlassian, ...) — tools surface as `<server>__<tool>` with namespaced audit |
| Web Dashboard | `forge ui` for browser-based agent management |
| Build Signing | Ed25519 artifact signing & verification |
| Air-Gap Ready | Runs with local models, no cloud required |
Expand Down
8 changes: 7 additions & 1 deletion docs/core-concepts/tools-and-builtins.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,18 @@ All file tools use `PathValidator` (from `pathutil.go`):

| Adapter | Description |
|---------|-------------|
| `mcp_call` | Call tools on MCP servers via JSON-RPC |
| `webhook_call` | POST JSON payloads to webhook URLs. Strips credentials on cross-origin redirects |
| `openapi_call` | Call OpenAPI-described endpoints |

Adapter tools bridge external services into the agent's tool set.

> **MCP tools** are not listed in this table. Configure MCP servers
> under the top-level `mcp:` block in `forge.yaml`; each server's
> discovered tools are registered as namespaced `<server>__<tool>`
> entries automatically. See [docs/mcp/](../mcp/index.md) for the
> configuration reference. The previous `mcp_call` adapter tool
> was removed in v0.12.0 — the new block is strictly more capable.

## Web Search Providers

The `web_search` tool supports two providers:
Expand Down
79 changes: 79 additions & 0 deletions docs/mcp/audit-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
title: "MCP — Audit Events"
description: "Seven event types emitted by the MCP subsystem; their fields and reason codes."
order: 4
---

Every MCP audit event is NDJSON to stderr alongside the existing
Forge audit stream. **No event ever carries argument or result
bytes** — only sizes, durations, server/tool names, and stable
reason codes. The grep-test
`TestMCPTool_Audit_NeverLogsBytes` pins this invariant.

## Event matrix

| Event | When | Fields |
|------------------------|--------------------------------------------|-------------------------------------------------|
| `mcp_server_started` | A server reaches `Ready` | `name`, `transport`, `tool_count` |
| `mcp_server_failed` | A server reaches terminal `Failed` | `name`, `phase`, `reason` |
| `mcp_server_degraded` | Transport error mid-call; entering backoff | `name`, `attempt`, `backoff_ms` |
| `mcp_tool_call` | Before every `tools/call` | `server`, `tool`, `args_size` |
| `mcp_tool_result` | After every `tools/call` | `server`, `tool`, `duration_ms`, `result_size`, `ok`, `reason?` |
| `mcp_tool_conflict` | Registry rejects a tool name | `incoming_name`, `error` |
| `mcp_token_refresh` | Every OAuth refresh attempt | `server`, `ok`, `reason` |

Every event also carries the standard top-level fields: `ts`,
`event`, `correlation_id` (when scoped to a request).

## Reason codes

### `mcp_tool_result.reason` (only when `ok=false`)

| Reason | Cause |
|----------------|----------------------------------------------------------------|
| `unavailable` | 5xx / network error / DNS / TLS / timeout |
| `protocol` | 4xx, malformed JSON-RPC frame, JSON-RPC error response |
| `revoked` | OAuth refresh denied (`invalid_grant`, `expired_token`) |
| `canceled` | Caller cancelled `ctx` (deadline exceeded or explicit cancel) |
| `tool_error` | MCP server set `isError: true` in `CallToolResult` |
| `unknown` | Anything else — investigate |

### `mcp_server_failed.phase`

| Phase | Meaning |
|-------------|----------------------------------------------------------|
| `connect` | HTTP dial / TCP refused / DNS / TLS |
| `initialize`| MCP `initialize` handshake; includes version mismatch |
| `discover` | `tools/list` failed or returned a malformed input schema |
| `runtime` | Anything else (transport error after Ready) |

### `mcp_server_failed.reason`

| Reason | Meaning |
|----------------------|----------------------------------------------------------|
| `backoff_exhausted` | 5 reconnect attempts failed (1s/2s/4s/8s/16s schedule) |
| `version_mismatch` | Server's `protocolVersion` ≠ `2025-06-18` (pinned) |

### `mcp_token_refresh.reason`

| Reason | Meaning |
|-------------------|--------------------------------------------------------|
| `refreshed` | New access token persisted |
| `refresh_denied` | Auth server returned `invalid_grant` / `expired_token` |
| `transport` | Network / 5xx talking to the token endpoint |
| `store_error` | Failed to persist the refreshed token |

## What to dashboard

For routine ops, three Grafana-style queries are enough:

1. **Per-server availability:** `count(mcp_server_started) - count(mcp_server_failed)`
2. **Tool latency:** `histogram_quantile(0.95, mcp_tool_result.duration_ms by tool)`
3. **OAuth refresh failures:** `count(mcp_token_refresh{ok="false"})` — page when non-zero.

## What NOT to log

Do not log `args` or result `text/data` content. The audit stream
intentionally omits these — Forge has no way to know whether a tool
argument is PII, secrets, or operationally sensitive data, so it
treats every byte as untrustworthy for logging.
82 changes: 82 additions & 0 deletions docs/mcp/cli-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
title: "MCP — CLI Reference"
description: "Every flag of every forge mcp subcommand."
order: 3
---

```
forge mcp list
forge mcp test <name> [--call <tool> --args '<json>' --timeout <dur>]
forge mcp login <name>
forge mcp logout <name>
```

## `forge mcp list`

Prints a table of every configured server with a quick reachability
probe. Always exits 0 — one server down is reported in its row, not
as a process error.

```
NAME TRANSPORT URL STATE TOOLS REASON
linear http https://mcp.linear.app/sse ready 12
notion http https://mcp.notion.com/sse ready 4
internal http http://internal.svc.cluster.local failed 0 mcp: transport unavailable: HTTP 503
```

## `forge mcp test <name>`

Connects to one server, runs `initialize` + `tools/list`, and prints
the discovered tools with truncated input schemas. Exits non-zero on
any failure (useful in CI).

Flags:
- `--call <tool>` — also invoke this tool after listing.
- `--args '<json>'` — JSON arguments for `--call`. Default `{}`.
- `--timeout <duration>` — per-RPC timeout. Default 10s.

Examples:

```sh
forge mcp test linear
forge mcp test linear --call list_issues --args '{"first":5}'
```

## `forge mcp login <name>`

Runs the OAuth 2.1 PKCE flow against the named server. Opens a
`127.0.0.1` loopback listener on a random port, opens the operator's
browser at the configured `authorize_url`, exchanges the returned
code for tokens, and persists them encrypted at
`~/.forge/credentials/mcp_<name>.json`.

Requires `auth.type: oauth` in the server's config. Fails fast for
bearer / static / no-auth servers.

For Kubernetes deployments:

```sh
# On your laptop:
forge mcp login linear

# Bundle the credentials file into a Secret:
kubectl create secret generic mcp-tokens \
--from-file=mcp_linear.json=$HOME/.forge/credentials/mcp_linear.json

# In the pod spec:
volumeMounts:
- name: mcp-tokens
mountPath: /etc/forge/credentials
readOnly: true
env:
- name: HOME
value: /etc/forge # so llm/oauth.LoadCredentials finds the file
```

## `forge mcp logout <name>`

Deletes the stored OAuth token for the server. Idempotent.

```sh
forge mcp logout linear
```
Loading
Loading