@procedures/mcp-server 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +211 -292
- package/dist/.tsbuildinfo +1 -1
- package/dist/host.d.ts +168 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +182 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/procedures/mcp/serve.d.ts +18 -0
- package/dist/procedures/mcp/serve.d.ts.map +1 -1
- package/dist/procedures/mcp/serve.js +36 -31
- package/dist/procedures/mcp/serve.js.map +1 -1
- package/dist/transport/http.d.ts +4 -0
- package/dist/transport/http.d.ts.map +1 -1
- package/dist/transport/http.js +9 -3
- package/dist/transport/http.js.map +1 -1
- package/dist/transport/mcp-transport.d.ts +26 -0
- package/dist/transport/mcp-transport.d.ts.map +1 -1
- package/dist/transport/mcp-transport.js +184 -19
- package/dist/transport/mcp-transport.js.map +1 -1
- package/dist/transport/stdio.d.ts +4 -2
- package/dist/transport/stdio.d.ts.map +1 -1
- package/dist/transport/stdio.js +4 -3
- package/dist/transport/stdio.js.map +1 -1
- package/dist/types.d.ts +70 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/untrusted.d.ts +56 -0
- package/dist/untrusted.d.ts.map +1 -0
- package/dist/untrusted.js +68 -0
- package/dist/untrusted.js.map +1 -0
- package/package.json +4 -4
- package/src/host.ts +346 -0
- package/src/index.ts +15 -0
- package/src/procedures/mcp/serve.ts +54 -36
- package/src/transport/http.ts +9 -3
- package/src/transport/mcp-transport.ts +206 -21
- package/src/transport/stdio.ts +5 -3
- package/src/types.ts +82 -2
- package/src/untrusted.ts +109 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @procedures/mcp-server
|
|
2
2
|
|
|
3
|
+
## 0.8.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2523b1b: One MCP host factory, `createMcpHost()`, and the questions of a procedure to the person at the caller (CLI-10, CLI-12). PATCH: new exports and new optional options before 1.0.0. The old exports and their behavior stay.
|
|
8
|
+
|
|
9
|
+
- `@procedures/mcp-server`: `createMcpHost(options)` builds the registry (or the modules), the tool surface, the read-only narrowing, the host policy, the `ProcedureServer` and one `McpServerTransport` for each face (stdio, Streamable HTTP, SDK transports). The stdio face stops the host when its input ends, and `host.closed` resolves. `mcp.serve` and the dev-tools server use the factory. Thus `mcp.serve` gets its options from `hostPolicy()`, and it takes `readOnly` and `instructions`.
|
|
10
|
+
- `@procedures/client`: `ctx.elicit` asks the person at the caller for input or a confirmation. The helpers `elicitInput()` and `elicitConfirmation()` give the action `unavailable` with a reason when no one can answer. `elicitWithPolicy()` adds the consents of the operator (`operatorConsent`), and `ask: false` turns the questions off. The operator answers only when the face cannot ask. A failed question of a face that can ask (a timeout, a lost connection) gives `unavailable`, and an abort gives `cancel`. `ServerRequest` and `InvokeOptions` get `elicit` and `progress`.
|
|
11
|
+
- `@procedures/client`: `ctx.progress` tells the caller the progress of the call of the host. `metadata.untrusted` declares the output fields with text from outside the host, and `metadata.title` gives the title of a tool. `metadata.toolHints` can make a tool look more dangerous than its effects (`readOnlyHint: false`, `idempotentHint: false`, `destructiveHint: true`, `openWorldHint: true`). The registry refuses each other value, and `toolHintsOf()` gives the hints with the override.
|
|
12
|
+
- `@procedures/mcp-server`: `McpServerTransport` answers `ctx.elicit` with MCP elicitation (form and URL), over stdio and Streamable HTTP. It sends `ctx.progress` as `notifications/progress`, and it gives the instructions of the server. An untrusted field goes in envelopes with a random id, and its text cannot close its envelope. The option `content` can give other text blocks for a result. The option `json: "compact"` gives the JSON of the default blocks on one line. The option `toolName` gives the name of each tool, so a server that moves to the host keeps its tool names. A call finds its procedure by the tool, not by the decoded name. Each tool call over Streamable HTTP carries the credentials of its HTTP request, so the request hook `principalOf` and the token check of the server apply to it. A server that reads cookies gets the CSRF rule on this face too.
|
|
13
|
+
- `@procedures/mcp`: a tool gets `title` from `metadata.title`. In the output schema, each untrusted field is a string.
|
|
14
|
+
- `@procedures/mcp-dev-tools`: `createDevToolsHost()` gives the MCP host as `mcp`. `dist/server.js` ends its process when the client closes the input of stdio. The tool list stays the same.
|
|
15
|
+
- Updated dependencies [2d3f235]
|
|
16
|
+
- Updated dependencies [1aa9ef7]
|
|
17
|
+
- Updated dependencies [2523b1b]
|
|
18
|
+
- Updated dependencies [c559b00]
|
|
19
|
+
- @procedures/client@0.8.1
|
|
20
|
+
- @procedures/mcp@0.8.1
|
|
21
|
+
|
|
3
22
|
## 0.8.0
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -2,365 +2,284 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@procedures/mcp-server)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
[](https://www.typescriptlang.org/)
|
|
7
|
-
[](https://nodejs.org/)
|
|
8
|
-
[](https://modelcontextprotocol.io/)
|
|
5
|
+
`@procedures/mcp-server` serves typed procedures as MCP tools to Claude and to other MCP clients. Its folder in the monorepo is `packages/client-mcp`.
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
- `createMcpHost()` builds a complete MCP host from a registry, a tool surface and a host policy.
|
|
8
|
+
- `McpServerTransport` is the MCP face of a `ProcedureServer`. The host uses it for each face.
|
|
9
|
+
- A procedure can ask the person at the caller for input or a confirmation (MCP elicitation).
|
|
10
|
+
- A procedure can tell the caller the progress of a long call. The caller can cancel a call.
|
|
11
|
+
- A procedure can declare the fields of its output that hold text from outside the host. The host shows that text in envelopes.
|
|
11
12
|
|
|
12
|
-
##
|
|
13
|
+
## Contents
|
|
13
14
|
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
15
|
+
- [Install](#install)
|
|
16
|
+
- [Start a host](#start-a-host)
|
|
17
|
+
- [The faces](#the-faces)
|
|
18
|
+
- [Questions to the person](#questions-to-the-person)
|
|
19
|
+
- [Progress and cancel](#progress-and-cancel)
|
|
20
|
+
- [Text from outside the host](#text-from-outside-the-host)
|
|
21
|
+
- [The result of a tool](#the-result-of-a-tool)
|
|
22
|
+
- [The transport](#the-transport)
|
|
23
|
+
- [The procedures of this package](#the-procedures-of-this-package)
|
|
24
|
+
- [Set up Claude](#set-up-claude)
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
## Overview
|
|
28
|
-
|
|
29
|
-
**client-mcp** bridges the procedure system with MCP (Model Context Protocol):
|
|
30
|
-
|
|
31
|
-
- **Transport Layer** - Connects `ProcedureServer` to Claude via stdio
|
|
32
|
-
- **Dynamic Registration** - Automatically refreshes tools when procedures change
|
|
33
|
-
- **Tool Filtering** - Control which procedures are exposed as MCP tools
|
|
34
|
-
- **Debug Mode** - Logging for troubleshooting MCP connections
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
## Installation
|
|
26
|
+
## Install
|
|
39
27
|
|
|
40
28
|
```bash
|
|
41
|
-
npm install @procedures/mcp-server
|
|
29
|
+
npm install @procedures/mcp-server @procedures/client
|
|
42
30
|
```
|
|
43
31
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
## Architecture
|
|
47
|
-
|
|
48
|
-
### System Overview
|
|
49
|
-
|
|
50
|
-
```mermaid
|
|
51
|
-
graph TB
|
|
52
|
-
subgraph "Claude Layer"
|
|
53
|
-
Claude[Claude Desktop / Code]
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
subgraph "MCP Protocol"
|
|
57
|
-
ListTools[ListToolsRequest]
|
|
58
|
-
CallTool[CallToolRequest]
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
subgraph "client-mcp"
|
|
62
|
-
Transport[McpServerTransport]
|
|
63
|
-
ToolMap[Tool Mapper]
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
subgraph "Procedure Layer"
|
|
67
|
-
Server[ProcedureServer]
|
|
68
|
-
Registry[PROCEDURE_REGISTRY]
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
subgraph "Procedures"
|
|
72
|
-
FS[fs.read]
|
|
73
|
-
Git[git.commit]
|
|
74
|
-
Shell[shell.run]
|
|
75
|
-
More[...]
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
Claude -->|stdio| ListTools
|
|
79
|
-
Claude -->|stdio| CallTool
|
|
80
|
-
ListTools --> Transport
|
|
81
|
-
CallTool --> Transport
|
|
82
|
-
Transport --> ToolMap
|
|
83
|
-
ToolMap --> Registry
|
|
84
|
-
Transport --> Server
|
|
85
|
-
Server --> FS
|
|
86
|
-
Server --> Git
|
|
87
|
-
Server --> Shell
|
|
88
|
-
Server --> More
|
|
89
|
-
```
|
|
32
|
+
The package needs Node.js 22 or later.
|
|
90
33
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
```mermaid
|
|
94
|
-
sequenceDiagram
|
|
95
|
-
participant Claude as Claude
|
|
96
|
-
participant MCP as McpServerTransport
|
|
97
|
-
participant Server as ProcedureServer
|
|
98
|
-
participant Registry as PROCEDURE_REGISTRY
|
|
99
|
-
participant Handler as Procedure Handler
|
|
100
|
-
|
|
101
|
-
Note over Claude,Handler: Tool Discovery
|
|
102
|
-
Claude->>MCP: ListToolsRequest
|
|
103
|
-
MCP->>Registry: getAll()
|
|
104
|
-
Registry-->>MCP: Procedure[]
|
|
105
|
-
MCP->>MCP: Convert to MCP tools
|
|
106
|
-
MCP-->>Claude: ListToolsResponse (tools[])
|
|
107
|
-
|
|
108
|
-
Note over Claude,Handler: Tool Execution
|
|
109
|
-
Claude->>MCP: CallToolRequest (fs.read)
|
|
110
|
-
MCP->>Server: handle({ path: ["fs", "read"], input })
|
|
111
|
-
Server->>Registry: get(["fs", "read"])
|
|
112
|
-
Registry-->>Server: Procedure
|
|
113
|
-
Server->>Handler: handler(input, ctx)
|
|
114
|
-
Handler-->>Server: Result
|
|
115
|
-
Server-->>MCP: Response
|
|
116
|
-
MCP-->>Claude: CallToolResponse
|
|
117
|
-
```
|
|
34
|
+
## Start a host
|
|
118
35
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
subgraph "Procedure System"
|
|
135
|
-
Server[ProcedureServer]
|
|
136
|
-
Convert[proceduresToMcpTools]
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
Init --> Connect
|
|
140
|
-
Connect --> Register
|
|
141
|
-
Register --> ListTools
|
|
142
|
-
Register --> CallTool
|
|
143
|
-
ListTools --> Convert
|
|
144
|
-
CallTool --> Server
|
|
36
|
+
`createMcpHost()` builds the registry, the expose rule, the host policy, the `ProcedureServer` and a transport for each face. It starts nothing: `start()` starts the faces.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { createRegistry } from "@procedures/client";
|
|
40
|
+
import { createMcpHost } from "@procedures/mcp-server";
|
|
41
|
+
|
|
42
|
+
const host = createMcpHost({
|
|
43
|
+
registry: createRegistry(graphModule),
|
|
44
|
+
surface: { namespaces: ["graph"] },
|
|
45
|
+
readOnly: process.argv.includes("--read-only"),
|
|
46
|
+
name: "graph",
|
|
47
|
+
version: "0.1.0",
|
|
48
|
+
});
|
|
49
|
+
await host.start();
|
|
50
|
+
console.error(`${host.tools().length} tools over stdio`);
|
|
145
51
|
```
|
|
146
52
|
|
|
147
|
-
|
|
53
|
+
The options:
|
|
148
54
|
|
|
149
|
-
|
|
55
|
+
| Option | Default | Function |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `registry` | the global registry | The procedures of the host. |
|
|
58
|
+
| `modules` | none | Modules of procedures. Without `registry`, the host makes a registry of them. With `registry`, the host registers them in it. |
|
|
59
|
+
| `surface` | each procedure | The tools: a surface (`{ namespaces, exclude }`), keys and patterns (`["git.status", "fs.*"]`), or a function of the path. A list of keys gives a subset of tools. |
|
|
60
|
+
| `exclude` | none | Keys that are not tools, also when the surface matches them. |
|
|
61
|
+
| `readOnly` | `false` | With `true`, the tools are only the procedures that change nothing. Each call has that capability. |
|
|
62
|
+
| `policy` | strict calls | More of the host policy (`hostPolicy()` of `@procedures/client`): `strictCalls`, `timeout`, `interceptors`, `capability`, `resources`, `token`, `browser`. Refer to [Strict calls](#strict-calls). |
|
|
63
|
+
| `interceptors` | none | Interceptors after `policy.interceptors`. A function gets the registry and the expose rule of the host. |
|
|
64
|
+
| `serverOptions` | none | The options of `ProcedureServer` that the policy does not give: `autoRegister`, `collections`, `idempotency`. |
|
|
65
|
+
| `name`, `version` | `procedure-server`, `1.0.0` | The server info in the MCP handshake. |
|
|
66
|
+
| `instructions` | none | The instructions of the server. The MCP client gives them to the model. |
|
|
67
|
+
| `faces` | `{ stdio: true }` | The faces of the host. Refer to [The faces](#the-faces). |
|
|
68
|
+
| `elicitation` | ask the person | The rules of the questions to the person. Refer to [Questions to the person](#questions-to-the-person). |
|
|
69
|
+
| `untrusted` | tag `untrusted-content` | The envelopes of text from outside the host. |
|
|
70
|
+
| `content` | none | A hook that gives other text blocks for a result. |
|
|
71
|
+
| `json` | `pretty` | The JSON of the default text blocks: `pretty` (an indent of 2 spaces) or `compact` (one line, fewer tokens). |
|
|
72
|
+
| `toolFilter` | none | More filters of the tools: tags and internal procedures. |
|
|
73
|
+
| `toolName` | `git.status` | The name of the tool of a procedure, a function of the path. `(path) => path.join("_")` gives `git_status`. A server that moves to the host keeps its tool names. |
|
|
74
|
+
| `streamLimits` | 100 items, 30 seconds | The limits of a stream that can continue without an end. |
|
|
75
|
+
| `debug` | `false` | With `true`, the host writes debug lines to stderr. |
|
|
150
76
|
|
|
151
|
-
|
|
152
|
-
import { ProcedureServer, PROCEDURE_REGISTRY } from "@procedures/client";
|
|
153
|
-
import { McpServerTransport } from "@procedures/mcp-server";
|
|
77
|
+
The host has these parts:
|
|
154
78
|
|
|
155
|
-
|
|
156
|
-
|
|
79
|
+
| Part | Function |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `registry`, `policy`, `server` | The registry, the `HostPolicy` and the `ProcedureServer` of the host. |
|
|
82
|
+
| `expose` | The expose rule: the procedures that are tools. |
|
|
83
|
+
| `transports` | One `McpServerTransport` for each face. |
|
|
84
|
+
| `start()`, `stop()` | These functions start and stop the faces. The calls in flight get the grace time. A host stops one time. |
|
|
85
|
+
| `closed` | A promise. It resolves when the host stopped. |
|
|
86
|
+
| `tools()`, `state()` | The tools of the host at this time, and the state of each face. |
|
|
157
87
|
|
|
158
|
-
|
|
159
|
-
const server = new ProcedureServer({
|
|
160
|
-
autoRegister: true,
|
|
161
|
-
registry: PROCEDURE_REGISTRY,
|
|
162
|
-
});
|
|
88
|
+
The host does not build the options of `ProcedureServer` by hand. `hostPolicy()` and `procedureServerOptions()` of `@procedures/client` give them. The dev-tools server (`mark mcp`) and `mcp.serve` use `createMcpHost()` too.
|
|
163
89
|
|
|
164
|
-
|
|
165
|
-
const mcp = new McpServerTransport(server, {
|
|
166
|
-
transport: "stdio",
|
|
167
|
-
serverInfo: { name: "my-server", version: "1.0.0" },
|
|
168
|
-
});
|
|
90
|
+
### Strict calls
|
|
169
91
|
|
|
170
|
-
|
|
171
|
-
server.addTransport(mcp);
|
|
172
|
-
await server.start();
|
|
173
|
-
```
|
|
92
|
+
Strict calls are the safe default of an MCP host. A procedure of code without `metadata.calls` can use only the tools of the host in its nested calls. Thus a tool cannot reach a procedure that the host does not expose. A procedure with `metadata.calls` can also use the procedures that it declares.
|
|
174
93
|
|
|
175
|
-
|
|
94
|
+
To turn strict calls off, give `policy: { strictCalls: false }`. Turn them off only for a host whose tools you trust with each procedure of the registry.
|
|
176
95
|
|
|
177
|
-
##
|
|
96
|
+
## The faces
|
|
178
97
|
|
|
179
|
-
|
|
98
|
+
Each face is one `McpServerTransport` on the server of the host. A host can have more than one face.
|
|
180
99
|
|
|
181
|
-
|
|
100
|
+
| Face | Function |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `stdio: true` | The standard input and output. Over stdio, `console.log` writes to stderr from `start()` on, because stdout carries the protocol. |
|
|
103
|
+
| `stdio: { stdin, stdout, stopOnEnd, redirectConsole }` | Other streams, for a test or an embedding. With `stopOnEnd: true` (the default), the host stops when the input ends. |
|
|
104
|
+
| `http: { port, host, path, token, allowedOrigins }` | Streamable HTTP. The host listens on `127.0.0.1` by default. Without a token, the face takes the token of the policy. |
|
|
105
|
+
| `sdk` | One or more MCP SDK transports, for example the `InMemoryTransport` of a test. |
|
|
182
106
|
|
|
183
|
-
|
|
184
|
-
interface McpServerTransportOptions {
|
|
185
|
-
transport: "stdio" | "sse"; // Transport type
|
|
186
|
-
serverInfo: {
|
|
187
|
-
name: string; // Server name
|
|
188
|
-
version: string; // Server version
|
|
189
|
-
};
|
|
190
|
-
filter?: McpToolFilter; // Tool filtering options
|
|
191
|
-
debug?: boolean; // Enable debug logging
|
|
192
|
-
}
|
|
107
|
+
When the client closes the input of stdio, the host stops, and `closed` resolves. The entry `dist/server.js` of `@procedures/mcp-dev-tools` then ends its process.
|
|
193
108
|
|
|
194
|
-
|
|
195
|
-
constructor(server: ProcedureServer, options: McpServerTransportOptions);
|
|
109
|
+
Each tool call over Streamable HTTP carries the credentials of its HTTP request: the bearer token, the cookies, the headers and the query. Thus the request hook of the policy (`principalOf`) and the token check of the server apply to each call. When the server reads cookies, the face trusts a request with a cookie only from a named origin (CSRF). A call over stdio comes from this process and has no credentials.
|
|
196
110
|
|
|
197
|
-
|
|
198
|
-
getState(): {
|
|
199
|
-
connected: boolean;
|
|
200
|
-
toolCount: number;
|
|
201
|
-
lastRefresh: Date;
|
|
202
|
-
};
|
|
111
|
+
## Questions to the person
|
|
203
112
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
113
|
+
A procedure can ask the person at the caller for input or a confirmation during a call. MCP elicitation is one face that answers. The shape is not specific to MCP: another face (a terminal) can answer the same questions.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { elicitConfirmation } from "@procedures/client";
|
|
117
|
+
|
|
118
|
+
handler: async (input, ctx) => {
|
|
119
|
+
const answer = await elicitConfirmation(ctx, `Did you give the URL ${input.url} yourself?`, {
|
|
120
|
+
consent: "user-provided",
|
|
121
|
+
});
|
|
122
|
+
if (!answer.confirmed) return { released: false, note: answer.reason ?? "The person said no." };
|
|
123
|
+
return { released: true, by: answer.by };
|
|
124
|
+
};
|
|
207
125
|
```
|
|
208
126
|
|
|
209
|
-
|
|
127
|
+
The parts in `@procedures/client`:
|
|
210
128
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
129
|
+
- `ctx.elicit` is the function of the face. It is absent when no one can answer. Its `modes` tells which kinds of question the face can ask: `form`, `url`.
|
|
130
|
+
- `elicitInput(ctx, request)` asks a question with a form of fields. The fields are the primitive fields of MCP: text, number, yes or no, and choices.
|
|
131
|
+
- `elicitConfirmation(ctx, question)` asks a question of yes or no. `confirmed` is true only for an accepted yes.
|
|
132
|
+
- The procedures that a procedure starts ask the same person.
|
|
215
133
|
|
|
216
|
-
|
|
134
|
+
Each answer has an `action`:
|
|
217
135
|
|
|
218
|
-
|
|
136
|
+
| Action | Meaning |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `accept` | The person, or the consent of the operator, gave the answer. `by` tells who: `person` or `operator`. |
|
|
139
|
+
| `decline` | The person said no. |
|
|
140
|
+
| `cancel` | The person closed the question without a choice, or the call aborted. |
|
|
141
|
+
| `unavailable` | No one can answer. `reason` tells why. The helpers do not throw for this case. |
|
|
219
142
|
|
|
220
|
-
|
|
143
|
+
The operator can give a consent when the server starts. Thus a server refuses input from the user without a consent of the operator. The host option `elicitation` has the rules:
|
|
221
144
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
pathPrefix?: string[]; // Only include procedures with this prefix
|
|
228
|
-
}
|
|
145
|
+
| Option | Default | Function |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `ask` | `true` | With `false`, the face does not ask the person, also when the client can. |
|
|
148
|
+
| `operatorConsent` | none | `true` for each question with a consent key, or a list of consent keys. |
|
|
149
|
+
| `timeout` | 5 minutes | The time that the face waits for the answer of the person. |
|
|
229
150
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
pathPrefix: ["api"],
|
|
236
|
-
},
|
|
151
|
+
```typescript
|
|
152
|
+
const allow = process.argv.includes("--allow-user-provided") || process.env.POLITE_CRAWLER_ALLOW_USER_PROVIDED === "1";
|
|
153
|
+
const host = createMcpHost({
|
|
154
|
+
registry,
|
|
155
|
+
elicitation: { operatorConsent: allow ? ["user-provided"] : [] },
|
|
237
156
|
});
|
|
238
157
|
```
|
|
239
158
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
```mermaid
|
|
243
|
-
graph TB
|
|
244
|
-
subgraph "All Procedures"
|
|
245
|
-
FS[fs.read]
|
|
246
|
-
Git[git.commit]
|
|
247
|
-
Internal[_internal.debug]
|
|
248
|
-
API[api.users.list]
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
subgraph "Filter: excludeInternal"
|
|
252
|
-
F1[fs.read]
|
|
253
|
-
F2[git.commit]
|
|
254
|
-
F3[api.users.list]
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
subgraph "Filter: pathPrefix api"
|
|
258
|
-
F4[api.users.list]
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
FS --> F1
|
|
262
|
-
Git --> F2
|
|
263
|
-
API --> F3
|
|
264
|
-
API --> F4
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
---
|
|
159
|
+
The order of the answers:
|
|
268
160
|
|
|
269
|
-
|
|
161
|
+
1. When the client can ask (it declares elicitation), the face asks the person. A decline of the person stays a decline.
|
|
162
|
+
2. When the question of the face fails (a timeout, a lost connection), the answer is `unavailable`. The operator does not answer: the person can see the question, and silence is not a consent.
|
|
163
|
+
3. When the client cannot ask, the consent of the operator answers. The operator answers only a form question with a consent key that the operator gave.
|
|
164
|
+
4. Else the answer is `unavailable`, with a reason.
|
|
270
165
|
|
|
271
|
-
|
|
166
|
+
Over Streamable HTTP, each question goes on the stream of its tool call.
|
|
272
167
|
|
|
273
|
-
|
|
274
|
-
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
168
|
+
## Progress and cancel
|
|
275
169
|
|
|
276
|
-
|
|
170
|
+
A procedure tells the caller the progress of a long call with `ctx.progress`:
|
|
277
171
|
|
|
278
|
-
```
|
|
279
|
-
{
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
"args": ["-y", "@procedures/mcp-dev-tools"]
|
|
284
|
-
}
|
|
172
|
+
```typescript
|
|
173
|
+
handler: async (input, ctx) => {
|
|
174
|
+
for (const [index, url] of input.urls.entries()) {
|
|
175
|
+
await fetchPage(url, ctx.signal);
|
|
176
|
+
await ctx.progress?.({ progress: index + 1, total: input.urls.length, message: url });
|
|
285
177
|
}
|
|
286
|
-
}
|
|
178
|
+
return { done: input.urls.length };
|
|
179
|
+
};
|
|
287
180
|
```
|
|
288
181
|
|
|
289
|
-
|
|
182
|
+
- `ctx.progress` is present only when the client sent a progress token. Each update becomes a `notifications/progress` message.
|
|
183
|
+
- Only the call of the host gets `ctx.progress`, not its nested calls. Thus the values of one call stay in order.
|
|
184
|
+
- The function does not reject. A client that is gone does not stop the call.
|
|
185
|
+
- A stream does not need it: the face gives each item of a stream as progress. When the procedure of a stream uses `ctx.progress`, the face gives only those updates.
|
|
186
|
+
- When the client cancels a call (`notifications/cancelled`), `ctx.signal` of the procedure aborts.
|
|
290
187
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
188
|
+
## Text from outside the host
|
|
189
|
+
|
|
190
|
+
A procedure that gives text from outside the host (a web page, a mail) declares the fields of its output that hold it. Then a model gets that text apart from the data, as data and not as instructions.
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { untrustedText } from "@procedures/client";
|
|
194
|
+
|
|
195
|
+
createProcedure()
|
|
196
|
+
.path(["crawl", "read"])
|
|
197
|
+
.meta({ description: "Read a stored page", untrusted: ["web_content"], effects: { reads: ["store"], writes: [] } })
|
|
198
|
+
.handler(async (input) => ({
|
|
199
|
+
doc: input.doc,
|
|
200
|
+
web_content: [untrustedText(page.text, { url: page.url, capture: page.capture })],
|
|
201
|
+
}));
|
|
304
202
|
```
|
|
305
203
|
|
|
306
|
-
|
|
204
|
+
- The value of an untrusted field is a text, an `UntrustedText` with its source, or a list of them.
|
|
205
|
+
- The first text block of the result has the data as JSON, without the untrusted fields.
|
|
206
|
+
- The second text block has a notice, then one envelope for each text: `<untrusted-content id="..." url="..." capture="...">`.
|
|
207
|
+
- A random id marks the start and the end of each envelope of one answer. A text cannot hold the name of the tag, in any case and with any dash or space. Thus a text cannot close its envelope.
|
|
208
|
+
- In the structured content and the output schema, each untrusted field is a string: the text of its envelopes.
|
|
209
|
+
- The host option `untrusted` changes the tag (`tag: "untrusted-web-content"`) and the notice. Its `wrap` function replaces the envelopes.
|
|
210
|
+
- `createUntrustedEnvelopes()` gives the same envelopes for other uses.
|
|
211
|
+
|
|
212
|
+
## The result of a tool
|
|
213
|
+
|
|
214
|
+
- A tool whose output schema is an object has an `outputSchema`. Each result then has `structuredContent`, and a text block for clients that do not read it.
|
|
215
|
+
- The hints of a tool (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) come from the declared `effects` of its procedure.
|
|
216
|
+
- `metadata.toolHints` of a procedure can make its tool look more dangerous than its effects. Refer to [Hint overrides](#hint-overrides).
|
|
217
|
+
- `metadata.title` of a procedure gives the `title` of its tool.
|
|
218
|
+
- A failed call gives `isError` with the code and the reason in `_meta`.
|
|
219
|
+
- The host option `content` can give other text blocks for a result that is a value. It gets the tool, the value and the default content, and it gives undefined for the default.
|
|
220
|
+
- The host option `json: "compact"` gives the JSON of the default blocks on one line. Use it in place of a content hook for that change only.
|
|
307
221
|
|
|
308
|
-
|
|
222
|
+
### Hint overrides
|
|
309
223
|
|
|
310
|
-
|
|
224
|
+
The declared effects stay the truth: the host enforces the effects, not the hints. An override can only make a tool look more dangerous:
|
|
311
225
|
|
|
312
|
-
|
|
226
|
+
| Hint | The only value of an override | Meaning |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| `readOnlyHint` | `false` | The tool can change something. |
|
|
229
|
+
| `idempotentHint` | `false` | A second call can change more. |
|
|
230
|
+
| `destructiveHint` | `true` | The tool can delete or overwrite data. |
|
|
231
|
+
| `openWorldHint` | `true` | The tool can reach systems outside the host. |
|
|
313
232
|
|
|
314
233
|
```typescript
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
234
|
+
createProcedure()
|
|
235
|
+
.path(["notes", "read"])
|
|
236
|
+
.meta({ description: "Read a shared note", effects: { reads: ["store"], writes: [] }, toolHints: { openWorldHint: true } });
|
|
318
237
|
```
|
|
319
238
|
|
|
320
|
-
|
|
239
|
+
The registry refuses each other value when the procedure registers, also when the effects give the same value. The type `ToolHintOverride` refuses it before the registry does. `toolHintsOf()` of `@procedures/client` gives the hints of a tool with its override.
|
|
321
240
|
|
|
322
|
-
|
|
241
|
+
## The transport
|
|
242
|
+
|
|
243
|
+
`McpServerTransport` is the MCP face of a `ProcedureServer`. `createMcpHost()` makes one for each face. Use it directly to add an MCP face to a server of your own:
|
|
323
244
|
|
|
324
245
|
```typescript
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
debug: true, // Logs to stderr
|
|
328
|
-
});
|
|
329
|
-
```
|
|
246
|
+
import { ProcedureServer, createRegistry } from "@procedures/client";
|
|
247
|
+
import { McpServerTransport } from "@procedures/mcp-server";
|
|
330
248
|
|
|
331
|
-
|
|
249
|
+
const registry = createRegistry(myModule);
|
|
250
|
+
const server = new ProcedureServer({ registry, autoRegister: true });
|
|
251
|
+
server.addTransport(new McpServerTransport(server, { registry, transport: "stdio", serverInfo: { name: "my-server", version: "1.0.0" } }));
|
|
252
|
+
await server.start();
|
|
253
|
+
```
|
|
332
254
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
255
|
+
- The tools follow the registry. When the list of tools changes, the transport sends `notifications/tools/list_changed`.
|
|
256
|
+
- The options `instructions`, `stdio`, `elicitation`, `untrusted` and `content` are the same as the options of the host.
|
|
257
|
+
- `toolFilter` selects the tools: `include` (a function of the path), `pathPrefix`, `includeTags`, `excludeTags` and `excludeInternal`.
|
|
258
|
+
- `transport: "http"` with `httpOptions` gives Streamable HTTP. `sdkTransport` connects an MCP SDK transport.
|
|
259
|
+
- `transport: "sse"` is not implemented.
|
|
338
260
|
|
|
339
|
-
|
|
340
|
-
ClientMCP[client-mcp<br/>McpServerTransport]
|
|
341
|
-
end
|
|
261
|
+
## The procedures of this package
|
|
342
262
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
263
|
+
| Procedure | Function |
|
|
264
|
+
|---|---|
|
|
265
|
+
| `mcp.serve` | This procedure starts an MCP host over stdio with `createMcpHost()`. Without `namespaces`, the tools come from `mcpNamespaces` and `mcpExclude` of the bundles (default `@procedures/bundle-mcp`). Without a list, no procedure is a tool. |
|
|
266
|
+
| `mcp.list-tools` | This procedure lists the procedures of the registry of the caller as MCP tools. |
|
|
347
267
|
|
|
348
|
-
|
|
349
|
-
ClientMCP --> MCP
|
|
350
|
-
ClientMCP --> Client
|
|
351
|
-
```
|
|
268
|
+
`exposeRule()`, `mergeSurfaces()` and `importBundles()` give the tool surface of some bundles.
|
|
352
269
|
|
|
353
|
-
|
|
270
|
+
## Set up Claude
|
|
354
271
|
|
|
355
|
-
|
|
272
|
+
`@procedures/mcp-dev-tools` is a ready MCP server with this package. Add it to the MCP configuration of Claude Code or Claude Desktop:
|
|
356
273
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
274
|
+
```json
|
|
275
|
+
{
|
|
276
|
+
"mcpServers": {
|
|
277
|
+
"dev-tools": { "command": "npx", "args": ["-y", "@procedures/mcp-dev-tools"] }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
362
281
|
|
|
363
|
-
|
|
282
|
+
The [README of mcp-dev-tools](../impl-mcp-dev/README.md) has the configuration.
|
|
364
283
|
|
|
365
284
|
## License
|
|
366
285
|
|