mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
* feat(mcp): forward authenticated user identity to opt-in STDIO MCP servers A STDIO MCP server is one shared subprocess per configuration; its env is fixed at spawn and STDIO has no per-request header channel, so per-user identity must travel in-band with each tool call. Previously nothing carried it, so an MCP server could not call its downstream REST backend on behalf of the acting user. Inject the authenticated username (from ToolExecutionContext) into each tool call's JSON arguments under the reserved key `__mateclaw_user__`, for servers an operator explicitly opts in via `mateclaw.mcp.identity-forward.servers` (by name or id). The MCP server reads/strips it and forwards on-behalf-of alongside its own backend API key. - McpIdentityForwardProperties: per-server opt-in allowlist (name or id). - IdentityForwardingToolCallback: wraps an MCP callback, merges the username into the args; injected by trusted code, overwrites any LLM-supplied value (no spoofing); forwards unchanged when there is no user or args aren't an object/are malformed. - McpClientManager: captures server names; wraps opt-in servers' callbacks inside the prefix wrapper (so name-prefixing / return-direct still see the raw delegate). Non-opt-in servers are untouched — username never leaks to them. - Tests: injection, LLM-value overwrite, empty/non-object/malformed inputs, no-user passthrough, opt-in matching by id/name. - Docs (zh/en mcp.md): opt-in config, `__mateclaw_user__` contract, FastMCP Python skeleton, trust model. Default off (empty allowlist) — zero behavior change for existing servers. Plaintext username suits a trusted-network REST backend keyed by an API key; a signed short-lived token is noted as the stronger-isolation follow-up. * feat(mcp): add signed-token trust model for MCP identity forwarding Plaintext username forwarding makes the REST backend trust an unverifiable assertion from the (shared, LLM-adjacent) MCP service — a confused-deputy model. Add an opt-in signed-token mode so identity crosses the trust boundary as a short-lived RS256 JWT the backend can verify with a public key. - McpIdentityForwardProperties: nested `token` config (enabled, issuer, ttl-seconds, key-id, private-key-pem, audiences) + USER_ARG/TOKEN_ARG keys. - McpIdentityForwardService: resolves the injection — plaintext username (__mateclaw_user__) when token mode off, else a minted RS256 JWT (__mateclaw_token__) with sub=user, aud=server, short exp, jti. Lazy key parse; fail-closed when token mode is on but the key is missing/unparseable (no silent downgrade to plaintext). Signs with MateClaw's private key so the backend only needs the public key (cannot mint/impersonate). - IdentityForwardingToolCallback: now delegates the what-to-inject decision to the service (keyed by per-server audience); static withClaim() keeps the JSON-merge logic (overwrites LLM-supplied key, leaves non-object/malformed args untouched). - McpClientManager: injects the service; passes service + audience through the wrap path for opt-in servers only. - Tests: token mint+verify (with an in-test RSA keypair, asserting sub/aud/iss/ exp/jti), plaintext mode, no-user and no-key fail-closed, audience resolution. - Docs (zh/en): token config, key generation, claims, REST-side verification example, public-key distribution + JWKS-endpoint follow-up. Default unchanged: token.enabled=false → plaintext (back-compat); whole feature still opt-in per server and off by default.
576 lines
22 KiB
Markdown
576 lines
22 KiB
Markdown
---
|
||
title: MCP Integration — Model Context Protocol Tool Extension
|
||
description: MateClaw acts as an MCP client, connecting to any external tool server via Model Context Protocol. JSON-RPC dynamic discovery, SSE/stdio dual transport, seamless unification with built-in tools.
|
||
head:
|
||
- - meta
|
||
- name: keywords
|
||
content: MCP,Model Context Protocol,MCP client,tool protocol,JSON-RPC,AI tool extension,Anthropic MCP
|
||
---
|
||
|
||
# MCP — Model Context Protocol
|
||
|
||
**MCP is how MateClaw talks to tools someone else built.**
|
||
|
||
Model Context Protocol is an open standard from Anthropic for connecting AI models to external tools and data. An MCP server is a process — local or remote — that advertises a set of tools over JSON-RPC. MateClaw acts as an MCP *client*: it connects, discovers tools via `tools/list`, and exposes them to your agents as if they were native. **From the agent's point of view, there's no difference between a built-in `@Tool` Spring bean and a tool coming from an MCP server.**
|
||
|
||
This is the escape hatch. If you need a capability MateClaw doesn't ship with — filesystem access for a sandboxed directory, Tavily search, a custom internal data service, a browser automation suite — there's probably already an MCP server for it, and you can plug it in without writing a line of Java.
|
||
|
||
---
|
||
|
||
## What MCP actually is
|
||
|
||
```
|
||
┌───────────────────────┐ ┌───────────────────────┐
|
||
│ MateClaw │ │ MCP Server │
|
||
│ (MCP Client) │ │ (Tool Provider) │
|
||
│ │ JSON-RPC │ │
|
||
│ Agent Engine ───────┼──────────────┼──► Tool A │
|
||
│ │ │ Tool B │
|
||
│ Tool Registry ◄──────┼──────────────┼─── Tool Discovery │
|
||
│ │ │ (tools/list) │
|
||
└───────────────────────┘ └───────────────────────┘
|
||
```
|
||
|
||
Core concepts:
|
||
|
||
- **MCP Client** — MateClaw, connecting to servers, discovering tools, forwarding invocations
|
||
- **MCP Server** — a third-party process declaring its available tools and executing calls
|
||
- **Tool Discovery** — the client sends `tools/list` to retrieve every tool and its parameter schema
|
||
- **Tool Invocation** — when the agent decides to call a tool, the client forwards the request to the right MCP server
|
||
|
||
New tool capabilities become available to agents **without modifying code or restarting the service**.
|
||
|
||
---
|
||
|
||
## Transport types
|
||
|
||
Three transports for different deployment scenarios:
|
||
|
||
### stdio (Standard I/O)
|
||
|
||
MateClaw spawns a local child process and exchanges JSON-RPC messages via stdin/stdout.
|
||
|
||
```
|
||
MateClaw ── stdin ──► MCP Server subprocess
|
||
◄─ stdout ──
|
||
```
|
||
|
||
**Use cases:** local Node.js/Python MCP packages (e.g., `@anthropic/mcp-filesystem`), command-line tool wrappers, development.
|
||
**Advantages:** no network configuration, works immediately, process isolation.
|
||
**Limitations:** local only.
|
||
|
||
### streamable_http (Streamable HTTP)
|
||
|
||
Standard HTTP POST for JSON-RPC, responses streamed back over HTTP. **Recommended for production.**
|
||
|
||
```
|
||
MateClaw ── HTTP POST ──► Remote MCP Server
|
||
◄─ HTTP Stream ──
|
||
```
|
||
|
||
**Use cases:** cloud-deployed MCP servers, deployments behind load balancers.
|
||
**Advantages:** standard HTTP, CDN/firewall friendly, auth headers.
|
||
|
||
### sse (Server-Sent Events)
|
||
|
||
Earlier HTTP transport using SSE for server-to-client push. Legacy compatibility; new projects should prefer `streamable_http`.
|
||
|
||
### Transport comparison
|
||
|
||
| Feature | stdio | streamable_http | sse |
|
||
|---------|-------|-----------------|-----|
|
||
| Deployment | Local only | Local or remote | Local or remote |
|
||
| Network requirement | None | HTTP reachable | HTTP reachable |
|
||
| Authentication | Environment variables | HTTP Headers | HTTP Headers |
|
||
| Process management | MateClaw manages subprocess | External | External |
|
||
| Recommendation | Local tools | Remote services | Legacy compatibility |
|
||
|
||
---
|
||
|
||
## Configuration via UI
|
||
|
||
`Tools → MCP Servers → Add MCP Server`. Fill in:
|
||
|
||
- **Name** — unique identifier (letters, numbers, `_`, `-`, `.`, spaces; 1–128 chars)
|
||
- **Description** — optional
|
||
- **Transport type** — `stdio`, `streamable_http`, or `sse`
|
||
- **Command** (stdio) — `npx`, `node`, `python`, etc.
|
||
- **Arguments** (stdio) — JSON array (e.g., `["-y", "@anthropic/mcp-filesystem", "/path"]`)
|
||
- **Working directory** (stdio) — optional
|
||
- **Environment variables** (stdio) — JSON object; supports `${ENV_VAR}` references
|
||
- **URL** (streamable_http/sse) — server endpoint
|
||
- **HTTP Headers** (streamable_http/sse) — JSON object (e.g., `{"Authorization": "Bearer token"}`)
|
||
- **Connect timeout** — default 30s
|
||
- **Read timeout** — default **60s** (raised from 30s in 1.5.0, #247; a single callTool round-trip that legitimately runs longer no longer gets cut off. Each server is tunable 5–300s)
|
||
|
||
Save. If enabled, MateClaw auto-attempts to connect and discover tools.
|
||
|
||
### Testing, enabling, status
|
||
|
||
- **Test Connection** — sends `tools/list`, returns result, latency, tool list
|
||
- **Enable/Disable toggle** — drop connection without deleting config
|
||
- **Status** — `connected` / `disconnected` / `error` with error detail
|
||
|
||
---
|
||
|
||
## Configuration via REST API
|
||
|
||
Full CRUD at `/api/v1/mcp/servers`.
|
||
|
||
### List all
|
||
|
||
```bash
|
||
curl -s http://localhost:18088/api/v1/mcp/servers \
|
||
-H "Authorization: Bearer <token>" | jq
|
||
```
|
||
|
||
Response includes `headersJson` and `envJson` automatically **sanitized** (`sk-****abcd`).
|
||
|
||
### Create — stdio
|
||
|
||
```bash
|
||
curl -X POST http://localhost:18088/api/v1/mcp/servers \
|
||
-H "Content-Type: application/json" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-d '{
|
||
"name": "filesystem",
|
||
"transport": "stdio",
|
||
"command": "npx",
|
||
"argsJson": "[\"-y\", \"@anthropic/mcp-filesystem\", \"/home/user/workspace\"]",
|
||
"enabled": true
|
||
}'
|
||
```
|
||
|
||
### Create — streamable_http
|
||
|
||
```bash
|
||
curl -X POST http://localhost:18088/api/v1/mcp/servers \
|
||
-H "Content-Type: application/json" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-d '{
|
||
"name": "remote-tools",
|
||
"transport": "streamable_http",
|
||
"url": "https://mcp.example.com/mcp",
|
||
"headersJson": "{\"Authorization\": \"Bearer your-api-key\"}",
|
||
"connectTimeoutSeconds": 15,
|
||
"readTimeoutSeconds": 60,
|
||
"enabled": true
|
||
}'
|
||
```
|
||
|
||
### Update (PATCH semantics)
|
||
|
||
```bash
|
||
curl -X PUT http://localhost:18088/api/v1/mcp/servers/{id} \
|
||
-H "Content-Type: application/json" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-d '{"description": "Updated", "readTimeoutSeconds": 60}'
|
||
```
|
||
|
||
After update, enabled servers automatically reconnect.
|
||
|
||
### Delete / Toggle / Test / Refresh
|
||
|
||
```bash
|
||
curl -X DELETE http://localhost:18088/api/v1/mcp/servers/{id} \
|
||
-H "Authorization: Bearer <token>"
|
||
|
||
curl -X PUT "http://localhost:18088/api/v1/mcp/servers/{id}/toggle?enabled=false" \
|
||
-H "Authorization: Bearer <token>"
|
||
|
||
curl -X POST http://localhost:18088/api/v1/mcp/servers/{id}/test \
|
||
-H "Authorization: Bearer <token>"
|
||
|
||
curl -X POST http://localhost:18088/api/v1/mcp/servers/refresh \
|
||
-H "Authorization: Bearer <token>"
|
||
```
|
||
|
||
**Built-in servers** (`builtin=true`) cannot be deleted.
|
||
|
||
---
|
||
|
||
## Practical examples
|
||
|
||
### Example 1 — Filesystem MCP (stdio)
|
||
|
||
```bash
|
||
curl -X POST http://localhost:18088/api/v1/mcp/servers \
|
||
-H "Content-Type: application/json" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-d '{
|
||
"name": "filesystem",
|
||
"description": "Filesystem access (restricted to specified directory)",
|
||
"transport": "stdio",
|
||
"command": "npx",
|
||
"argsJson": "[\"-y\", \"@anthropic/mcp-filesystem\", \"/home/user/workspace\"]",
|
||
"enabled": true
|
||
}'
|
||
```
|
||
|
||
Discovered tools: `read_file`, `write_file`, `list_directory`, `search_files`, `get_file_info`.
|
||
|
||
Security: `@anthropic/mcp-filesystem` only allows access to the specified directory and subdirectories.
|
||
|
||
### Example 2 — Remote HTTP with auth
|
||
|
||
```bash
|
||
curl -X POST http://localhost:18088/api/v1/mcp/servers \
|
||
-H "Content-Type: application/json" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-d '{
|
||
"name": "internal-data-service",
|
||
"transport": "streamable_http",
|
||
"url": "https://mcp-api.internal.example.com/mcp",
|
||
"headersJson": "{\"Authorization\": \"Bearer sk-your-api-key\", \"X-Team-Id\": \"engineering\"}",
|
||
"connectTimeoutSeconds": 10,
|
||
"readTimeoutSeconds": 120,
|
||
"enabled": true
|
||
}'
|
||
```
|
||
|
||
**Header values support environment variable references**: `{"Authorization": "Bearer ${MCP_API_KEY}"}` is replaced at runtime, **secrets don't land in the database**.
|
||
|
||
### Example 3 — Tavily search (stdio + env vars)
|
||
|
||
```bash
|
||
curl -X POST http://localhost:18088/api/v1/mcp/servers \
|
||
-H "Content-Type: application/json" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-d '{
|
||
"name": "tavily-search",
|
||
"transport": "stdio",
|
||
"command": "npx",
|
||
"argsJson": "[\"-y\", \"@anthropic/mcp-tavily\"]",
|
||
"envJson": "{\"TAVILY_API_KEY\": \"${TAVILY_API_KEY}\"}",
|
||
"enabled": true
|
||
}'
|
||
```
|
||
|
||
---
|
||
|
||
## How MCP tools become available to agents
|
||
|
||
```
|
||
Application startup
|
||
│
|
||
▼
|
||
Iterate enabled MCP servers
|
||
│
|
||
▼
|
||
Connect by transport → initialize → list tools → cache
|
||
│
|
||
▼
|
||
Tool registry (aggregates built-in tools + MCP tools)
|
||
│
|
||
▼
|
||
Agent tool set
|
||
```
|
||
|
||
**Key:** the agent fetches the **latest** active tool list on every invocation, so adding or removing MCP servers takes effect **without restarting**. From the agent's perspective, **MCP tools and built-in tools are identical** — no difference.
|
||
|
||
---
|
||
|
||
## Per-agent tool binding
|
||
|
||
::: tip New in 1.3.0
|
||
Before v1.2.0, all employees could call every MCP tool by default — it was a global switch. v1.3.0 makes the binding **per-employee**, and adds dirty-state detection plus namespace collision handling.
|
||
:::
|
||
|
||
### Three problems it solves
|
||
|
||
**Problem 1: Tool namespace collisions.**
|
||
Two MCP servers both expose `read_file` — which one wins? v1.3.0 internally uses a **stable server-prefixed callback name** (`{serverName}__{toolName}`) and persists it to `mate_mcp_server.tools_cache_json`. The picker shows them as `serverA__read_file` and `serverB__read_file`; the agent's prompt maps them back to original names to save tokens and avoid LLM confusion.
|
||
|
||
**Problem 2: MCP server / tool rename breaks bindings.**
|
||
In v1.2.0, renaming a server orphaned every employee bound to it. v1.3.0 introduces a **persistent tool cache**: every successful list-tools writes tool metadata to a `tools_cache_json` JSON column on `mate_mcp_server`. When validating bindings and the server is temporarily unreachable, the cache is consulted as fallback — bindings stay marked `stale` and become live again the moment the server reconnects.
|
||
|
||
**Problem 3: Save silently accepted non-existent tool references.**
|
||
A typo'd `nonexistent-server.weird-tool` would save fine and blow up at runtime. v1.3.0 runs `AgentBindingService.validate(...)` on save:
|
||
|
||
| Status | Meaning | Save behavior |
|
||
|---|---|---|
|
||
| `connected` | Server online, tool visible | ✅ Persist normally |
|
||
| `stale` | Server temporarily offline but in cache | ✅ Persist (marked stale) |
|
||
| `unavailable` | Server disabled | ✅ Persist (marked unavailable) |
|
||
| `orphan` | Server / tool no longer exists at all | ❌ Reject save, prompt user to clear |
|
||
|
||
### Where to see tool status
|
||
|
||
`Agents → pick employee → Tools` — see [Agent tool binding](./agents#tool-binding-per-agent-tool-picker).
|
||
|
||
### Data contract
|
||
|
||
- `mate_mcp_server.tools_cache_json` (new column in v1.3.0): JSON array, each element `{name, description, inputSchema, lastSeenAt}`
|
||
- `mate_agent_tool.tool_name`: stores the **prefixed callback name** `{serverName}__{toolName}` rather than the raw name, so a server rename surfaces immediately as an observable join miss
|
||
- `AgentBindingService.getEffectiveToolNames(agentId)` is the single source of truth for tool dispatch — runs every turn, ensuring the editor view and the runtime view always agree
|
||
|
||
### Server-side rules
|
||
|
||
- MCP servers bridged in via ACP **cannot** be edited from the MCP server list (they're owned by the ACP server's own lifecycle)
|
||
- A tool marked `unavailable` is **not listed** in the agent's system prompt — the LLM won't reach for it, but the binding row is preserved
|
||
- `returnDirect=true` tools (whose output replaces the assistant turn) go through the same ACL — they **do not bypass** binding
|
||
|
||
---
|
||
|
||
## Connection management
|
||
|
||
### Automatic connection on startup
|
||
|
||
All `enabled=true` MCP servers connect automatically when the app starts. A single server's failure doesn't block other servers or application startup.
|
||
|
||
### Thread safety
|
||
|
||
The active-client map is concurrent, with an independent lock per server.
|
||
|
||
### Connection replacement
|
||
|
||
**"Connect new, then disconnect old"** strategy: build a new client, initialize it, swap it into the pool, close the old one. If the new client fails, the old one remains.
|
||
|
||
### Subprocess cleanup
|
||
|
||
For stdio servers, cleanup happens on: disable/delete, config replacement, application shutdown (`@PreDestroy`), connection failure.
|
||
|
||
### Status monitoring
|
||
|
||
After each connection operation, results persist:
|
||
|
||
- `last_status` — `connected` / `disconnected` / `error`
|
||
- `last_error` — error message
|
||
- `last_connected_time` — timestamp of last success
|
||
- `tool_count` — currently discovered tools
|
||
|
||
### Manual refresh
|
||
|
||
`POST /api/v1/mcp/servers/refresh` drops all existing connections and reconnects every enabled server. Useful for troubleshooting.
|
||
|
||
---
|
||
|
||
## Database storage — `mate_mcp_server`
|
||
|
||
| Column | Type | Default | Purpose |
|
||
|--------|------|---------|---------|
|
||
| `id` | BIGINT | — | Primary key |
|
||
| `name` | VARCHAR(128) | — | Unique identifier |
|
||
| `description` | TEXT | NULL | Server description |
|
||
| `transport` | VARCHAR(32) | `stdio` | `stdio` / `streamable_http` / `sse` |
|
||
| `url` | VARCHAR(512) | NULL | Remote URL |
|
||
| `headers_json` | TEXT | NULL | HTTP headers JSON |
|
||
| `command` | VARCHAR(512) | NULL | Startup command |
|
||
| `args_json` | TEXT | NULL | Command arguments JSON array |
|
||
| `env_json` | TEXT | NULL | Environment variables JSON; supports `${VAR}` |
|
||
| `cwd` | VARCHAR(512) | NULL | Working directory |
|
||
| `enabled` | BOOLEAN | TRUE | On/off |
|
||
| `connect_timeout_seconds` | INT | 30 | HTTP connect timeout |
|
||
| `read_timeout_seconds` | INT | 60 | Request response timeout (default 60 since 1.5.0, was 30) |
|
||
| `last_status` | VARCHAR(32) | `disconnected` | Last connection status |
|
||
| `last_error` | TEXT | NULL | Last error message |
|
||
| `last_connected_time` | DATETIME | NULL | Last successful connection |
|
||
| `tool_count` | INT | 0 | Discovered tool count |
|
||
| `builtin` | BOOLEAN | FALSE | Whether it's a built-in server |
|
||
| `create_time` / `update_time` | DATETIME | — | Timestamps |
|
||
| `deleted` | INT | 0 | Logical delete |
|
||
|
||
### Sensitive data sanitization
|
||
|
||
`headers_json` and `env_json` values are automatically masked in API responses. `args_json` is returned as-is.
|
||
|
||
### Environment variable references
|
||
|
||
- `${VAR_NAME}` — exact match and replacement
|
||
- `$VAR_NAME` — regex match
|
||
|
||
Keeps secrets out of the database.
|
||
|
||
---
|
||
|
||
## Forwarding the user's identity to an MCP server (on-behalf-of)
|
||
|
||
A STDIO MCP server is **one shared subprocess per configuration**, used by every
|
||
user; its environment is fixed at spawn and STDIO has no per-request header
|
||
channel like HTTP. So per-user identity **cannot travel via env** — it must ride
|
||
in-band with each tool call.
|
||
|
||
MateClaw can inject the **authenticated username** into every tool call for a
|
||
chosen server, so the server can call its downstream REST backend on behalf of
|
||
that user.
|
||
|
||
### Enable (opt-in, per server)
|
||
|
||
Off by default — injecting into every server would leak the username to any
|
||
third-party MCP server. Enable per server by **name or id**:
|
||
|
||
```yaml
|
||
mateclaw:
|
||
mcp:
|
||
identity-forward:
|
||
servers:
|
||
- my-internal-api # server name in mate_mcp_server
|
||
- 1000000042 # or the numeric server id
|
||
```
|
||
|
||
### Data contract
|
||
|
||
When enabled, MateClaw injects the reserved argument **`__mateclaw_user__`**
|
||
(value = authenticated username) into each tool call's JSON arguments. It is
|
||
injected by trusted server code, **never by the LLM** — any model-supplied value
|
||
of the same key is overwritten, so the model cannot spoof identity. When there is
|
||
no authenticated user, nothing is injected (identity is never fabricated).
|
||
|
||
The MCP server reads and strips the key, then calls REST with it plus its own
|
||
backend API key (e.g. an `X-On-Behalf-Of` header):
|
||
|
||
```python
|
||
# FastMCP example: MCP server as a Python CLI script (STDIO)
|
||
import os, httpx
|
||
from mcp.server.fastmcp import FastMCP
|
||
|
||
mcp = FastMCP("my-internal-api")
|
||
REST_BASE = os.environ["REST_BASE"]
|
||
API_KEY = os.environ["BACKEND_API_KEY"] # service-level key (authenticates the MCP service)
|
||
|
||
@mcp.tool()
|
||
def query_orders(keyword: str, __mateclaw_user__: str | None = None) -> str:
|
||
if not __mateclaw_user__:
|
||
raise ValueError("missing injected identity") # reject identity-less calls
|
||
headers = {
|
||
"Authorization": f"ApiKey {API_KEY}", # service identity
|
||
"X-On-Behalf-Of": __mateclaw_user__, # the acting user
|
||
}
|
||
r = httpx.get(f"{REST_BASE}/orders", params={"q": keyword}, headers=headers, timeout=30)
|
||
r.raise_for_status()
|
||
return r.text
|
||
|
||
if __name__ == "__main__":
|
||
mcp.run() # STDIO
|
||
```
|
||
|
||
> If a tool's input schema is `additionalProperties: false`, declare
|
||
> `__mateclaw_user__` as an optional parameter (as above) or strict validation
|
||
> will reject it.
|
||
|
||
### Two trust models
|
||
|
||
**① Plaintext (default)**: injects the plaintext username. Fits a trusted
|
||
network where the backend authenticates the MCP service by API key and treats
|
||
the forwarded user as on-behalf-of. The backend trusts the raw string.
|
||
|
||
**② Signed token (recommended across a trust boundary)**: injects a short-lived
|
||
**RS256 JWT** that MateClaw signs with a private key (reserved key becomes
|
||
**`__mateclaw_token__`**); the REST backend **verifies it with the public key**,
|
||
so it trusts the signature — not the MCP service, the Python script, or the
|
||
transport.
|
||
|
||
```yaml
|
||
mateclaw:
|
||
mcp:
|
||
identity-forward:
|
||
servers:
|
||
- my-internal-api
|
||
token:
|
||
enabled: true
|
||
issuer: mateclaw
|
||
ttl-seconds: 60 # short, tens of seconds
|
||
key-id: mateclaw-mcp-1
|
||
private-key-pem: ${MCP_IDFWD_PRIVATE_KEY_PEM:} # PKCS#8 PEM (RS256 private key)
|
||
audiences: # optional; default aud = server name
|
||
my-internal-api: https://api.internal
|
||
```
|
||
|
||
Generate the key pair (private → MateClaw, public → REST backend):
|
||
|
||
```bash
|
||
openssl genpkey -algorithm RSA -pkcs8 -out mcp-idfwd-private.pem
|
||
openssl pkey -in mcp-idfwd-private.pem -pubout -out mcp-idfwd-public.pem
|
||
# private-key-pem takes the private key body (PEM headers optional; stripped on parse)
|
||
```
|
||
|
||
Token claims: `iss`, `sub`=user, `aud`=this server, `iat`, `exp` (short), `jti`.
|
||
`aud` + short `exp` bound replay to tens of seconds and to one backend. **When
|
||
token mode is on but no key is configured, it fails closed** (no token minted,
|
||
nothing injected — the backend rejects) rather than silently downgrading to
|
||
plaintext.
|
||
|
||
> `sub` carries the MateClaw user identifier (`ChatOrigin.requesterId`). If your
|
||
> backend authorizes on an immutable numeric id, resolve username→id before
|
||
> minting (kept decoupled from the user store here).
|
||
|
||
The MCP server (Python) only forwards — it does not verify:
|
||
|
||
```python
|
||
@mcp.tool()
|
||
def query_orders(keyword: str, __mateclaw_token__: str | None = None) -> str:
|
||
if not __mateclaw_token__:
|
||
raise ValueError("missing identity token")
|
||
headers = {"Authorization": f"Bearer {__mateclaw_token__}"} # forward to REST
|
||
return httpx.get(f"{REST_BASE}/orders", params={"q": keyword}, headers=headers, timeout=30).text
|
||
```
|
||
|
||
REST backend verifies (pseudocode):
|
||
|
||
```python
|
||
import jwt # PyJWT
|
||
claims = jwt.decode(token, public_key_pem, algorithms=["RS256"],
|
||
issuer="mateclaw", audience="https://api.internal")
|
||
user = claims["sub"] # trusted only after signature verification
|
||
# → per-user authorization; invalid/expired → 401
|
||
```
|
||
|
||
> Public-key distribution: for now an operator configures the public key on the
|
||
> REST side out-of-band. A JWKS endpoint for auto-distribution + rotation is a
|
||
> natural follow-up.
|
||
>
|
||
> Relationship to the API key: you can keep the API key as service/channel auth
|
||
> ("this MCP service may talk to the backend") plus the JWT as the user
|
||
> assertion — two clean layers — or let the JWT carry both.
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
### "Command not found" (stdio)
|
||
|
||
1. Confirm the command is in PATH of the user running MateClaw
|
||
2. Verify: `which npx` or `npx --version`
|
||
3. Docker: confirm command is installed in the container
|
||
4. Use full path: `/usr/local/bin/npx`
|
||
|
||
### Connection timeout
|
||
|
||
1. HTTP/SSE: confirm URL reachable (`curl -v <url>`)
|
||
2. Check firewall rules
|
||
3. Increase `connectTimeoutSeconds` / `readTimeoutSeconds`
|
||
4. stdio: first `npx -y` run may need to download packages
|
||
|
||
### SSL/TLS errors
|
||
|
||
1. Confirm remote SSL certificate is valid and not expired
|
||
2. Self-signed: add CA cert to JVM trust store
|
||
3. Confirm JDK supports required TLS version
|
||
|
||
### Tools not showing up
|
||
|
||
1. Check `tool_count > 0`
|
||
2. Use test connection, confirm `discoveredTools` non-empty
|
||
3. Verify MCP server implements `tools/list`
|
||
4. Check backend logs for MCP tool-discovery output
|
||
|
||
### Tool invocation failures
|
||
|
||
1. Check backend logs for specific errors
|
||
2. Confirm MCP server process is running (stdio)
|
||
3. Confirm remote server reachable (HTTP/SSE)
|
||
4. Check `readTimeoutSeconds` is sufficient
|
||
5. Try refresh connections
|
||
|
||
### Orphaned subprocesses (stdio)
|
||
|
||
Subprocesses are cleaned up on normal shutdown. If MateClaw was force-killed (`kill -9`), subprocesses may remain. `ps aux | grep mcp` and terminate.
|
||
|
||
---
|
||
|
||
## Next
|
||
|
||
- [Tools](./tools) — how MCP tools relate to built-in tools
|
||
- [Skills](./skills) — MCP-backed skills
|
||
- [Configuration](./config) — full configuration reference
|