@procedures/mcp-server 0.7.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +211 -292
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/host.d.ts +168 -0
  5. package/dist/host.d.ts.map +1 -0
  6. package/dist/host.js +182 -0
  7. package/dist/host.js.map +1 -0
  8. package/dist/index.d.ts +6 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +5 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/procedures/mcp/serve.d.ts +18 -0
  13. package/dist/procedures/mcp/serve.d.ts.map +1 -1
  14. package/dist/procedures/mcp/serve.js +36 -31
  15. package/dist/procedures/mcp/serve.js.map +1 -1
  16. package/dist/transport/http.d.ts +4 -0
  17. package/dist/transport/http.d.ts.map +1 -1
  18. package/dist/transport/http.js +9 -3
  19. package/dist/transport/http.js.map +1 -1
  20. package/dist/transport/mcp-transport.d.ts +26 -0
  21. package/dist/transport/mcp-transport.d.ts.map +1 -1
  22. package/dist/transport/mcp-transport.js +184 -19
  23. package/dist/transport/mcp-transport.js.map +1 -1
  24. package/dist/transport/stdio.d.ts +4 -2
  25. package/dist/transport/stdio.d.ts.map +1 -1
  26. package/dist/transport/stdio.js +4 -3
  27. package/dist/transport/stdio.js.map +1 -1
  28. package/dist/types.d.ts +70 -2
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/untrusted.d.ts +56 -0
  31. package/dist/untrusted.d.ts.map +1 -0
  32. package/dist/untrusted.js +68 -0
  33. package/dist/untrusted.js.map +1 -0
  34. package/package.json +4 -4
  35. package/src/host.ts +346 -0
  36. package/src/index.ts +15 -0
  37. package/src/procedures/mcp/serve.ts +54 -36
  38. package/src/transport/http.ts +9 -3
  39. package/src/transport/mcp-transport.ts +206 -21
  40. package/src/transport/stdio.ts +5 -3
  41. package/src/types.ts +82 -2
  42. package/src/untrusted.ts +109 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
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
+
22
+ ## 0.8.0
23
+
24
+ ### Patch Changes
25
+
26
+ - Updated dependencies [4b09a03]
27
+ - Updated dependencies [4b09a03]
28
+ - Updated dependencies [aa6e3aa]
29
+ - Updated dependencies [698e24d]
30
+ - Updated dependencies [4b09a03]
31
+ - Updated dependencies [600c497]
32
+ - Updated dependencies [7c2e7f7]
33
+ - Updated dependencies [90ea741]
34
+ - Updated dependencies [c4cfa34]
35
+ - Updated dependencies [d25805a]
36
+ - @procedures/client@0.8.0
37
+ - @procedures/mcp@0.8.0
38
+
3
39
  ## 0.7.0
4
40
 
5
41
  ### Patch Changes
package/README.md CHANGED
@@ -2,365 +2,284 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@procedures/mcp-server?color=2346c4&label=npm)](https://www.npmjs.com/package/@procedures/mcp-server)
4
4
 
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
7
- [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D22-green.svg)](https://nodejs.org/)
8
- [![MCP](https://img.shields.io/badge/MCP-Protocol-purple.svg)](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
- > MCP server transport for the procedure system. Exposes procedures as MCP tools for Claude integration.
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
- ## Table of Contents
13
+ ## Contents
13
14
 
14
- - [Overview](#overview)
15
- - [Installation](#installation)
16
- - [Architecture](#architecture)
17
- - [Quick Start](#quick-start)
18
- - [API Reference](#api-reference)
19
- - [Tool Filtering](#tool-filtering)
20
- - [Claude Desktop Setup](#claude-desktop-setup)
21
- - [Integration](#integration)
22
- - [Requirements](#requirements)
23
- - [License](#license)
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
- ### Request Flow
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
- ### Transport Architecture
120
-
121
- ```mermaid
122
- graph LR
123
- subgraph "McpServerTransport"
124
- Init[Initialize]
125
- Connect[Connect stdio]
126
- Register[Register handlers]
127
- end
128
-
129
- subgraph "MCP Handlers"
130
- ListTools[tools/list]
131
- CallTool[tools/call]
132
- end
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
- ## Quick Start
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
- ```typescript
152
- import { ProcedureServer, PROCEDURE_REGISTRY } from "@procedures/client";
153
- import { McpServerTransport } from "@procedures/mcp-server";
77
+ The host has these parts:
154
78
 
155
- // Load procedures (via side-effect imports)
156
- import "@procedures/bundle-dev/register"; // registers its procedures
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
- // Create procedure server
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
- // Create MCP transport
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
- // Add transport and start
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
- ## API Reference
96
+ ## The faces
178
97
 
179
- ### McpServerTransport
98
+ Each face is one `McpServerTransport` on the server of the host. A host can have more than one face.
180
99
 
181
- Main transport class for MCP integration.
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
- ```typescript
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
- class McpServerTransport {
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
- // Get current transport state
198
- getState(): {
199
- connected: boolean;
200
- toolCount: number;
201
- lastRefresh: Date;
202
- };
111
+ ## Questions to the person
203
112
 
204
- // Close the transport
205
- close(): Promise<void>;
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
- ### Built-in Procedures
127
+ The parts in `@procedures/client`:
210
128
 
211
- | Path | Description |
212
- |------|-------------|
213
- | `mcp.serve` | Start MCP server programmatically |
214
- | `mcp.list-tools` | List available MCP tools with filtering |
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
- ## Tool Filtering
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
- Control which procedures are exposed as MCP tools:
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
- ```typescript
223
- interface McpToolFilter {
224
- includeTags?: string[]; // Only include these tags
225
- excludeTags?: string[]; // Exclude these tags
226
- excludeInternal?: boolean; // Exclude internal procedures
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
- const mcp = new McpServerTransport(server, {
231
- transport: "stdio",
232
- filter: {
233
- includeTags: ["public"],
234
- excludeInternal: true,
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
- ### Filtering Examples
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
- ## Claude Desktop Setup
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
- ### Configuration File Locations
166
+ Over Streamable HTTP, each question goes on the stream of its tool call.
272
167
 
273
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
274
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
168
+ ## Progress and cancel
275
169
 
276
- ### Basic Configuration
170
+ A procedure tells the caller the progress of a long call with `ctx.progress`:
277
171
 
278
- ```json
279
- {
280
- "mcpServers": {
281
- "my-server": {
282
- "command": "npx",
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
- ### With Custom Bundle
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
- ```json
292
- {
293
- "mcpServers": {
294
- "dev-tools": {
295
- "command": "node",
296
- "args": ["C:/Users/me/git/client/packages/impl-mcp-dev/dist/server.js"],
297
- "env": {
298
- "MCP_BUNDLES": "@procedures/bundle-mcp",
299
- "MCP_DEBUG": "true"
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
- ## Integration
222
+ ### Hint overrides
309
223
 
310
- ### Dynamic Tool Registration
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
- The transport automatically refreshes MCP tools when procedures are registered or unregistered:
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
- // Tools are automatically updated when procedures change
316
- PROCEDURE_REGISTRY.register(newProcedure); // → MCP tools refresh
317
- PROCEDURE_REGISTRY.unregister(oldPath); // → MCP tools refresh
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
- ### Debug Mode
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
- Enable debug logging for troubleshooting:
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
- const mcp = new McpServerTransport(server, {
326
- transport: "stdio",
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
- ### With Other Packages
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
- ```mermaid
334
- graph TB
335
- subgraph "Implementation"
336
- ImplMCP[impl-mcp-dev<br/>Ready-to-run server]
337
- end
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
- subgraph "Transport"
340
- ClientMCP[client-mcp<br/>McpServerTransport]
341
- end
261
+ ## The procedures of this package
342
262
 
343
- subgraph "Core"
344
- MCP[mcp<br/>Types & utilities]
345
- Client[client<br/>ProcedureServer]
346
- end
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
- ImplMCP --> ClientMCP
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
- ## Requirements
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
- - **Node.js** >= 22
358
- - **Dependencies:**
359
- - `@procedures/client`
360
- - `@procedures/mcp`
361
- - `@modelcontextprotocol/sdk`
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