@gjermundgaraba/effect-actions 0.10.0 → 0.12.0
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/README.md +103 -43
- package/dist/Action.d.mts +31 -118
- package/dist/Action.mjs +15 -53
- package/dist/ActionCli.d.mts +60 -28
- package/dist/ActionCli.mjs +54 -48
- package/dist/ActionHttp.d.mts +18 -67
- package/dist/ActionHttp.mjs +20 -70
- package/dist/ActionMcp.d.mts +25 -31
- package/dist/ActionMcp.mjs +140 -74
- package/dist/ActionRpc.d.mts +147 -0
- package/dist/ActionRpc.mjs +113 -0
- package/dist/ActionToolkit.d.mts +5 -8
- package/dist/ActionToolkit.mjs +3 -3
- package/dist/Authentication.d.mts +15 -15
- package/dist/Authentication.mjs +42 -96
- package/dist/Testing.d.mts +32 -25
- package/dist/Testing.mjs +45 -30
- package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
- package/dist/authentication/provider.mjs +12 -0
- package/dist/authentication/refusal.mjs +36 -0
- package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
- package/dist/cli/commands.mjs +302 -0
- package/dist/{internal → contract}/call.d.mts +2 -2
- package/dist/contract/call.mjs +10 -0
- package/dist/{internal → contract}/errors.d.mts +13 -10
- package/dist/{internal → contract}/errors.mjs +9 -14
- package/dist/{internal → contract}/implementation.d.mts +37 -21
- package/dist/{internal → contract}/implementation.mjs +18 -49
- package/dist/contract/media.d.mts +13 -0
- package/dist/contract/media.mjs +18 -0
- package/dist/contract/rules.d.mts +25 -0
- package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
- package/dist/{internal → http}/client.d.mts +7 -9
- package/dist/http/client.mjs +17 -0
- package/dist/http/declared.mjs +20 -0
- package/dist/http/schema-errors.mjs +9 -0
- package/dist/mcp/protocol.d.mts +17 -0
- package/dist/mcp/protocol.mjs +149 -0
- package/dist/stdio/console.mjs +64 -0
- package/dist/{internal → testing}/memory.d.mts +1 -1
- package/dist/{internal → testing}/memory.mjs +5 -17
- package/dist/toolkit/tools.mjs +29 -0
- package/docs/Action-failures.md +47 -0
- package/docs/Action-in-process.md +93 -0
- package/docs/Action.md +206 -424
- package/docs/ActionCli-failures.md +102 -0
- package/docs/ActionCli.md +206 -292
- package/docs/ActionHttp-failures.md +83 -0
- package/docs/ActionHttp.md +202 -281
- package/docs/ActionMcp-failures.md +31 -0
- package/docs/ActionMcp.md +229 -193
- package/docs/ActionRpc-failures.md +21 -0
- package/docs/ActionRpc.md +178 -0
- package/docs/ActionToolkit-failures.md +15 -0
- package/docs/ActionToolkit.md +100 -117
- package/docs/Authentication-advanced.md +139 -0
- package/docs/Authentication-failures.md +99 -0
- package/docs/Authentication.md +142 -392
- package/docs/CONTEXT.md +39 -23
- package/docs/README.md +71 -35
- package/docs/Testing-failures.md +20 -0
- package/docs/Testing.md +186 -151
- package/docs/guarantees.md +35 -95
- package/docs/setup-failures.md +34 -0
- package/docs/setup.md +88 -87
- package/examples/README.md +68 -0
- package/examples/assistant/README.md +92 -0
- package/examples/assistant/src/agent.ts +92 -0
- package/examples/assistant/src/cli.ts +34 -0
- package/examples/assistant/src/commands.ts +25 -0
- package/examples/assistant/src/identity/Identity.ts +33 -0
- package/examples/assistant/src/identity/authorize.ts +24 -0
- package/examples/assistant/src/identity/handlers.ts +10 -0
- package/examples/assistant/src/identity/members.ts +9 -0
- package/examples/assistant/src/identity/permissions.ts +23 -0
- package/examples/assistant/src/scripted.ts +23 -0
- package/examples/assistant/src/tasks/Tasks.ts +63 -0
- package/examples/assistant/src/tasks/handlers.ts +25 -0
- package/examples/assistant/src/tasks/store.ts +96 -0
- package/examples/assistant/src/toolkit.ts +30 -0
- package/examples/assistant/test/commands.test.ts +57 -0
- package/examples/assistant/test/identity/identity.test.ts +43 -0
- package/examples/assistant/test/tasks/store.test.ts +48 -0
- package/examples/assistant/test/tasks/tasks.test.ts +75 -0
- package/examples/assistant/test/toolkit.test.ts +121 -0
- package/examples/kitchen-sink/README.md +317 -0
- package/examples/kitchen-sink/src/Actions.ts +19 -0
- package/examples/kitchen-sink/src/Api.ts +24 -0
- package/examples/kitchen-sink/src/Rpc.ts +12 -0
- package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
- package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
- package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
- package/examples/kitchen-sink/src/admin/grants.ts +22 -0
- package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
- package/examples/kitchen-sink/src/admin/keys.ts +8 -0
- package/examples/kitchen-sink/src/agent.ts +47 -0
- package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
- package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
- package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
- package/examples/kitchen-sink/src/assistant/handlers.ts +146 -0
- package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
- package/examples/kitchen-sink/src/attachments/download.ts +49 -0
- package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
- package/examples/kitchen-sink/src/attachments/store.ts +58 -0
- package/examples/kitchen-sink/src/browser.ts +33 -0
- package/examples/kitchen-sink/src/cli.ts +85 -0
- package/examples/kitchen-sink/src/config.ts +18 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
- package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
- package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
- package/examples/kitchen-sink/src/identity/directory.ts +52 -0
- package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
- package/examples/kitchen-sink/src/identity/members.ts +13 -0
- package/examples/kitchen-sink/src/identity/quota.ts +62 -0
- package/examples/kitchen-sink/src/issues/Issues.ts +109 -0
- package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
- package/examples/kitchen-sink/src/issues/guide.ts +37 -0
- package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
- package/examples/kitchen-sink/src/issues/store.ts +96 -0
- package/examples/kitchen-sink/src/issues/write-limit.ts +41 -0
- package/examples/kitchen-sink/src/memory.ts +30 -0
- package/examples/kitchen-sink/src/ops.ts +42 -0
- package/examples/kitchen-sink/src/request-policy.ts +63 -0
- package/examples/kitchen-sink/src/routes.ts +120 -0
- package/examples/kitchen-sink/src/sdk.ts +46 -0
- package/examples/kitchen-sink/src/seed.ts +65 -0
- package/examples/kitchen-sink/src/server.ts +62 -0
- package/examples/kitchen-sink/src/stdio.ts +34 -0
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
- package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
- package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
- package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
- package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
- package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
- package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
- package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
- package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
- package/examples/kitchen-sink/test/routes.test.ts +123 -0
- package/examples/kitchen-sink/test/support/scripted.ts +40 -0
- package/examples/live-rpc/README.md +107 -0
- package/examples/live-rpc/src/Rpc.ts +8 -0
- package/examples/live-rpc/src/browser.ts +98 -0
- package/examples/live-rpc/src/chat/Chat.ts +75 -0
- package/examples/live-rpc/src/chat/authorize.ts +16 -0
- package/examples/live-rpc/src/chat/handlers.ts +50 -0
- package/examples/live-rpc/src/chat/room.ts +23 -0
- package/examples/live-rpc/src/chat/store.ts +54 -0
- package/examples/live-rpc/src/client.ts +37 -0
- package/examples/live-rpc/src/identity/Identity.ts +19 -0
- package/examples/live-rpc/src/identity/members.ts +8 -0
- package/examples/live-rpc/src/identity/verifier.ts +23 -0
- package/examples/live-rpc/src/routes.ts +22 -0
- package/examples/live-rpc/src/server.ts +43 -0
- package/examples/live-rpc/test/browser.test.ts +105 -0
- package/examples/live-rpc/test/chat/chat.test.ts +69 -0
- package/examples/live-rpc/test/routes.test.ts +99 -0
- package/examples/live-rpc/test/server.test.ts +75 -0
- package/examples/mcp-tools/README.md +136 -0
- package/examples/mcp-tools/src/instructions.ts +12 -0
- package/examples/mcp-tools/src/printers/Printers.ts +119 -0
- package/examples/mcp-tools/src/printers/fake.ts +115 -0
- package/examples/mcp-tools/src/printers/farm.ts +136 -0
- package/examples/mcp-tools/src/printers/handlers.ts +29 -0
- package/examples/mcp-tools/src/routes.ts +52 -0
- package/examples/mcp-tools/src/server.ts +30 -0
- package/examples/mcp-tools/src/stdio.ts +26 -0
- package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
- package/examples/mcp-tools/test/routes.test.ts +202 -0
- package/examples/notes-service/README.md +108 -0
- package/examples/notes-service/src/Api.ts +13 -0
- package/examples/notes-service/src/browser.ts +41 -0
- package/examples/notes-service/src/cli.ts +22 -0
- package/examples/notes-service/src/commands.ts +89 -0
- package/examples/notes-service/src/identity/Identity.ts +31 -0
- package/examples/notes-service/src/identity/authorize.ts +18 -0
- package/examples/notes-service/src/identity/handlers.ts +10 -0
- package/examples/notes-service/src/identity/users.ts +9 -0
- package/examples/notes-service/src/identity/verifier.ts +36 -0
- package/examples/notes-service/src/notes/Notes.ts +91 -0
- package/examples/notes-service/src/notes/handlers.ts +33 -0
- package/examples/notes-service/src/notes/store.ts +73 -0
- package/examples/notes-service/src/routes.ts +45 -0
- package/examples/notes-service/src/server.ts +26 -0
- package/examples/notes-service/src/status/Status.ts +10 -0
- package/examples/notes-service/src/status/handlers.ts +14 -0
- package/examples/notes-service/src/stdio.ts +25 -0
- package/examples/notes-service/test/commands.test.ts +134 -0
- package/examples/notes-service/test/notes/notes.test.ts +103 -0
- package/examples/notes-service/test/routes.test.ts +84 -0
- package/examples/quickstart/README.md +31 -0
- package/examples/quickstart/src/Api.ts +5 -0
- package/examples/quickstart/src/client.ts +13 -0
- package/examples/quickstart/src/greeting/Greeting.ts +12 -0
- package/examples/quickstart/src/greeting/handlers.ts +6 -0
- package/examples/quickstart/src/routes.ts +11 -0
- package/examples/quickstart/src/server.ts +16 -0
- package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
- package/package.json +32 -14
- package/dist/internal/actions.d.mts +0 -9
- package/dist/internal/authentication.mjs +0 -11
- package/dist/internal/call.mjs +0 -12
- package/dist/internal/cli.mjs +0 -328
- package/dist/internal/client.mjs +0 -25
- package/dist/internal/console.mjs +0 -77
- package/dist/internal/declared.d.mts +0 -13
- package/dist/internal/declared.mjs +0 -34
- package/dist/internal/mcp.d.mts +0 -11
- package/dist/internal/mcp.mjs +0 -52
- package/dist/internal/refusal.mjs +0 -67
- package/dist/internal/schema-errors.mjs +0 -20
- package/dist/internal/tools.mjs +0 -44
package/docs/ActionMcp.md
CHANGED
|
@@ -1,223 +1,259 @@
|
|
|
1
1
|
# ActionMcp
|
|
2
2
|
|
|
3
|
-
MCP tools from implementations, on Effect's native `McpServer
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
MCP tools from implementations, on Effect's native `McpServer`: one tool per served action, named
|
|
4
|
+
after it. `layerHttp` mounts a Streamable HTTP endpoint, for clients that connect to a URL;
|
|
5
|
+
`runStdio` is the whole program of a subprocess a local agent starts. For a model your own code
|
|
6
|
+
calls in process, use [ActionToolkit.md](ActionToolkit.md).
|
|
7
|
+
|
|
8
|
+
Read this page alone, in one tool call: pages read together are cut. When something fails,
|
|
9
|
+
search [ActionMcp-failures.md](ActionMcp-failures.md). Reading order: [README.md](README.md).
|
|
7
10
|
|
|
8
11
|
## API
|
|
9
12
|
|
|
10
13
|
Import `@gjermundgaraba/effect-actions/ActionMcp`.
|
|
11
14
|
|
|
12
|
-
| API | Purpose
|
|
13
|
-
| ------------------------------------- |
|
|
14
|
-
| `layerHttp(implementations, options)` |
|
|
15
|
-
| `runStdio(implementations, options)` |
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`McpServer.layerHttp` / `McpServer.layerStdio` options, except `protocols`, plus `actions` and `features`, and for `layerHttp`, `authentication`. The native ones pass through unchanged; `path` gains a default. Each implementation brings its
|
|
19
|
-
authorization ([Action.md](Action.md#implementations)); an endpoint serving protected actions
|
|
20
|
-
names their authentication descriptor and requires its provider
|
|
21
|
-
([Authentication.md](Authentication.md)).
|
|
22
|
-
|
|
23
|
-
| Option | Meaning |
|
|
24
|
-
| ------------------------------------ | ------------------------------------------------------------------------------ |
|
|
25
|
-
| `name`, `version` | Required native server information. |
|
|
26
|
-
| `description`, `websiteUrl`, `icons` | Optional native server information, sent to clients with `name` and `version`. |
|
|
27
|
-
| `instructions` | Optional native server instructions. |
|
|
28
|
-
| `extensions` | Optional native server capability extensions. |
|
|
29
|
-
| `path` | HTTP endpoint path; HTTP only, defaults to `/mcp`. |
|
|
30
|
-
| `allowedOrigins` | Optional exact Origin allowlist; HTTP only, not CORS configuration. |
|
|
31
|
-
| `authentication` | HTTP only: the protected tools' descriptor, required where one is served. |
|
|
32
|
-
| `actions` | Optional actions that are tools, among the implementations'; defaults to all. |
|
|
33
|
-
| `features` | Optional native resources, prompts and tools served beside the actions' tools. |
|
|
34
|
-
|
|
35
|
-
`implementations` is one implementation or a list. Every tool declares its action's errors plus the
|
|
36
|
-
built-in `InvalidInput`, `Unauthenticated` and `Forbidden`. `layerHttp`'s layer and `runStdio`'s
|
|
37
|
-
program retain the build failures and requirements of their builders, plus native
|
|
38
|
-
`IllegalArgumentError`. HTTP needs the router, the descriptor's provider where it serves a
|
|
39
|
-
protected action, and wraps handler and authorizer services as request requirements until middleware provided around it provides them, the
|
|
40
|
-
identity excepted, which the provider supplies. stdio needs `Stdio` and the caller's request
|
|
41
|
-
services, the identity of its protected tools included. Native `McpRequestContext`
|
|
42
|
-
is supplied by the server, not required of the host. The server also fails as its `features` do
|
|
43
|
-
and needs what they need, except the registry, `McpServer.McpServer`, which the endpoint provides,
|
|
44
|
-
such as to a feature registering with `McpServer.registerResource`.
|
|
45
|
-
|
|
46
|
-
## Canonical
|
|
47
|
-
|
|
48
|
-
One endpoint serves the public and the protected tools: discovery and `status` answer anyone,
|
|
49
|
-
and every other request authenticates before the endpoint decodes it.
|
|
50
|
-
|
|
51
|
-
```ts example=mcp.ts
|
|
52
|
-
import { Layer } from "effect";
|
|
53
|
-
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
54
|
-
import { authenticate } from "./authentication.js";
|
|
55
|
-
import { Login } from "./binding.js";
|
|
56
|
-
import { double, status, userActions } from "./handlers.js";
|
|
57
|
-
|
|
58
|
-
const allowedOrigins = ["http://localhost:3000", "http://127.0.0.1:3000"];
|
|
59
|
-
|
|
60
|
-
// One URL: discovery and `status` answer anyone; a protected tool's call authenticates
|
|
61
|
-
// before its arguments are decoded, with the 401 an MCP client signs in on.
|
|
62
|
-
export const layer = ActionMcp.layerHttp([status, userActions, double], {
|
|
63
|
-
name: "effect-actions",
|
|
64
|
-
version: "0.0.0",
|
|
65
|
-
allowedOrigins,
|
|
66
|
-
authentication: Login,
|
|
67
|
-
}).pipe(Layer.provide(authenticate));
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Cross-origin browsers
|
|
15
|
+
| API | Purpose |
|
|
16
|
+
| ------------------------------------- | ------------------------------------------------------------ |
|
|
17
|
+
| `layerHttp(implementations, options)` | A Layer serving the actions as tools at one HTTP endpoint. |
|
|
18
|
+
| `runStdio(implementations, options)` | An Effect serving them on standard I/O until stdin closes. |
|
|
19
|
+
| `Options` | Type of `runStdio`'s options. |
|
|
20
|
+
| `LayerHttpOptions` | Type of `layerHttp`'s options: `Options` and three for HTTP. |
|
|
71
21
|
|
|
72
|
-
`
|
|
73
|
-
native router CORS outside the route middleware. This CORS layer is global to the router;
|
|
74
|
-
choose its policy for every route it covers.
|
|
22
|
+
`implementations` is one implementation or a list.
|
|
75
23
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
24
|
+
| Option | Meaning |
|
|
25
|
+
| ------------------------------------ | ---------------------------------------------------------- |
|
|
26
|
+
| `name`, `version` | Required server information. |
|
|
27
|
+
| `description`, `websiteUrl`, `icons` | Optional server information. |
|
|
28
|
+
| `instructions` | Optional text for the model about the server as a whole. |
|
|
29
|
+
| `extensions` | Optional native capability extensions. |
|
|
30
|
+
| `actions` | The implementations' actions that are tools; default all. |
|
|
31
|
+
| `features` | One Layer of native resources, prompts and tools to serve. |
|
|
32
|
+
| `path` | HTTP only: the endpoint's path; defaults to `/mcp`. |
|
|
33
|
+
| `allowedOrigins` | HTTP only: the exact origins allowed; none by default. |
|
|
34
|
+
| `authentication` | HTTP only: the protected tools' authentication descriptor. |
|
|
82
35
|
|
|
83
|
-
|
|
36
|
+
All but `actions`, `features` and `authentication` are Effect's own `McpServer` options, passed
|
|
37
|
+
through. Its `protocols` and `allowSessionTermination` are not options.
|
|
84
38
|
|
|
85
|
-
|
|
39
|
+
## Serve tools over HTTP
|
|
86
40
|
|
|
87
|
-
|
|
41
|
+
Merge `layerHttp` into the routes, beside any other surface, and serve them with
|
|
42
|
+
`HttpRouter.serve`:
|
|
88
43
|
|
|
89
|
-
|
|
90
|
-
// This example is public; an endpoint serving protected actions takes their `authentication`.
|
|
44
|
+
```ts
|
|
91
45
|
export const routes = Layer.mergeAll(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
allowedOrigins,
|
|
95
|
-
allowedMethods: ["POST"],
|
|
96
|
-
allowedHeaders: [
|
|
97
|
-
"Content-Type",
|
|
98
|
-
"Authorization",
|
|
99
|
-
"MCP-Protocol-Version",
|
|
100
|
-
"MCP-Method",
|
|
101
|
-
"MCP-Name",
|
|
102
|
-
],
|
|
103
|
-
exposedHeaders: ["WWW-Authenticate", "MCP-Protocol-Version"],
|
|
104
|
-
}),
|
|
46
|
+
ActionHttp.layer(Http, greeting),
|
|
47
|
+
ActionMcp.layerHttp(greeting, { name: "greetings", version: "1.0.0" }),
|
|
105
48
|
);
|
|
106
49
|
```
|
|
107
50
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
51
|
+
See [quickstart/src/routes.ts](../examples/quickstart/src/routes.ts) and
|
|
52
|
+
[quickstart/src/server.ts](../examples/quickstart/src/server.ts). Surfaces given the same
|
|
53
|
+
implementations in one set of routes run each builder once, so a note written over HTTP is read
|
|
54
|
+
as a tool: [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts). For two
|
|
55
|
+
servers, see [Action-in-process.md](Action-in-process.md#share-one-build-across-servers).
|
|
56
|
+
|
|
57
|
+
- The endpoint speaks MCP 2026-07-28, 2025-11-25 and 2025-06-18, as the client negotiates.
|
|
58
|
+
Claude Code 2.1 and Codex CLI 0.162 both connect, as tested in October 2026:
|
|
59
|
+
[notes-service](../examples/notes-service/README.md#run-it) has the commands,
|
|
60
|
+
[mcp-tools](../examples/mcp-tools/README.md#connect-a-client) those for stdio and Claude Desktop.
|
|
61
|
+
- **A client of a 2025 revision opens a session** with `initialize`; a 2026-07-28 request stands
|
|
62
|
+
alone. A session holds what its client sent there and no identity: each request authenticates,
|
|
63
|
+
as its own caller. It lasts until its client ends it with a `DELETE`, answered 204, or the process stops, as
|
|
64
|
+
Effect expires none, so limit request bodies, which bounds what one holds. Where an endpoint
|
|
65
|
+
serves a protected tool, only a signed-in caller opens one, and that client signs in for every
|
|
66
|
+
request, a public tool's call included. An endpoint of public tools alone lets anyone open them.
|
|
67
|
+
- An endpoint without `authentication` answers whoever reaches its port. Bind it to loopback or to
|
|
68
|
+
a network only trusted clients are on, and limit request bodies
|
|
69
|
+
([ActionHttp.md](ActionHttp.md#serve-the-routes)):
|
|
70
|
+
[mcp-tools/src/server.ts](../examples/mcp-tools/src/server.ts).
|
|
71
|
+
- The endpoint takes no layer middleware. A limit that must hold for tools goes in the handlers:
|
|
72
|
+
[kitchen-sink/src/issues/write-limit.ts](../examples/kitchen-sink/src/issues/write-limit.ts).
|
|
73
|
+
|
|
74
|
+
## Serve tools over stdio
|
|
75
|
+
|
|
76
|
+
Provide `Stdio` and the startup services, then run it:
|
|
116
77
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const status = Action.implement(Status, () =>
|
|
125
|
-
Effect.log("status called").pipe(Effect.as({ ready: true })),
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
// Serves until the host closes stdin, then exits 0. Protocol messages use stdout
|
|
129
|
-
// exclusively: runStdio writes its program's Effect logs and `Console` output to stderr, and
|
|
130
|
-
// `logToStderr`, applied last, does so for the layers provided around it, and reports a failure
|
|
131
|
-
// there rather than as runMain would, on stdout.
|
|
132
|
-
ActionMcp.runStdio(status, { name: "effect-actions-stdio", version: "0.1.0" }).pipe(
|
|
133
|
-
Effect.provide(NodeStdio.layer),
|
|
78
|
+
```ts
|
|
79
|
+
ActionMcp.runStdio([status, notes, identity], { name: "notes-service", version: "1.0.0" }).pipe(
|
|
80
|
+
// DEMO ONLY: a real host reads who started it, from its own login or configuration.
|
|
81
|
+
Effect.provideService(CurrentUser, users.ada),
|
|
82
|
+
Effect.provide([NodeStdio.layer, NoteStore.layerMemory]),
|
|
134
83
|
ActionCli.logToStderr,
|
|
135
84
|
NodeRuntime.runMain,
|
|
136
85
|
);
|
|
137
86
|
```
|
|
138
87
|
|
|
139
|
-
|
|
88
|
+
See [notes-service/src/stdio.ts](../examples/notes-service/src/stdio.ts), and
|
|
89
|
+
[mcp-tools/src/stdio.ts](../examples/mcp-tools/src/stdio.ts) for public tools.
|
|
140
90
|
|
|
141
|
-
|
|
142
|
-
|
|
91
|
+
To try a server by hand, send it one request, its 2026-07-28 metadata in `_meta` as
|
|
92
|
+
`Testing.mcpRequest` builds it. Over HTTP it also needs all five headers:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
BODY='{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"getStatus","arguments":{},"_meta":{"io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}'
|
|
96
|
+
echo "$BODY" | node --import tsx examples/notes-service/src/stdio.ts
|
|
97
|
+
curl http://127.0.0.1:3000/mcp -d "$BODY" -H 'Content-Type: application/json' \
|
|
98
|
+
-H 'Accept: application/json, text/event-stream' -H 'MCP-Protocol-Version: 2026-07-28' \
|
|
99
|
+
-H 'MCP-Method: tools/call' -H 'MCP-Name: getStatus'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- **stdout carries the protocol alone.** `runStdio` sends its own program's Effect logs and
|
|
103
|
+
`Console` output to stderr. For the layers provided around it, apply `ActionCli.logToStderr`
|
|
104
|
+
last, before `runMain`: it moves their default logger and reports a startup failure there too.
|
|
105
|
+
Nothing moves the global `console.log`: keep it off stdout.
|
|
106
|
+
- **A subprocess has its own services.** It runs the handlers itself, so it shares a server's
|
|
107
|
+
data only through a store both reach, never one in memory. A host that should see what the
|
|
108
|
+
server holds connects to its HTTP endpoint.
|
|
109
|
+
- **No token arrives, so the host names the caller.** stdio takes no `authentication`: provide the
|
|
110
|
+
protected tools' identity around `runStdio`, whose one caller is whoever started it. Each
|
|
111
|
+
implementation's `authorize` still runs, so give that caller what it checks. Tools served
|
|
112
|
+
behind a token over HTTP keep their one protected contract here: never a public copy. An HTTP
|
|
113
|
+
server has many callers: never provide an identity at its startup, where it would stand for
|
|
114
|
+
every request.
|
|
115
|
+
- **Test the tools without a process**: the implementations in process over a fake upstream
|
|
116
|
+
([mcp-tools/test/printers/printers.test.ts](../examples/mcp-tools/test/printers/printers.test.ts)),
|
|
117
|
+
an endpoint in memory
|
|
118
|
+
([mcp-tools/test/routes.test.ts](../examples/mcp-tools/test/routes.test.ts)).
|
|
119
|
+
|
|
120
|
+
## Describe tools to a model
|
|
121
|
+
|
|
122
|
+
A tool is its contract ([Action.md](Action.md#define-a-contract)): a model knows only the action's
|
|
123
|
+
name, its `description`, and its input schema with each field's `description`. `readOnly` is the
|
|
124
|
+
tool's read-only hint, and the contract's `mcp` option holds its `title`, its other hints and its
|
|
125
|
+
`_meta`. The server's `instructions` say what the tools are for together. See
|
|
126
|
+
[mcp-tools/src/printers/Printers.ts](../examples/mcp-tools/src/printers/Printers.ts) and
|
|
127
|
+
[mcp-tools/src/instructions.ts](../examples/mcp-tools/src/instructions.ts).
|
|
128
|
+
|
|
129
|
+
- MCP takes a write as destructive unless it states `destructiveHint: false`.
|
|
130
|
+
- A tool's input is one object with keys. Omit `input` for a tool without arguments, and wrap a
|
|
131
|
+
union, an array or a scalar in a field.
|
|
132
|
+
- An argument the schema refuses, an undeclared one included, is answered by the native server
|
|
133
|
+
before the handler runs, as an `isError` result whose text is `Invalid parameters for tool
|
|
134
|
+
'<name>': …`, not `InvalidInput`. So constrain in the schema
|
|
135
|
+
what a model may send: an id written into an upstream URL is not made safe by
|
|
136
|
+
`encodeURIComponent`, which leaves `..` alone.
|
|
137
|
+
- A `Schema.makeFilter` check adds nothing to the tool's listed input schema, and
|
|
138
|
+
`Schema.isPattern` its `pattern` only for a RegExp with the `u` flag. Say such a rule in the
|
|
139
|
+
field's `description`, annotated before the check: after a custom one it is lost too.
|
|
140
|
+
- A declared error reaches the model as an `isError` result whose text is the error's JSON. Write
|
|
141
|
+
its `message` for the model, which acts on it:
|
|
142
|
+
[mcp-tools/src/printers/farm.ts](../examples/mcp-tools/src/printers/farm.ts). Every other case:
|
|
143
|
+
[guarantees.md](guarantees.md#wire-behavior).
|
|
144
|
+
- `actions` lists the tools, and only listed actions must suit MCP:
|
|
145
|
+
[kitchen-sink/src/Actions.ts](../examples/kitchen-sink/src/Actions.ts). Leave out an action a
|
|
146
|
+
model should not call, or one that takes a secret ([Action.md](Action.md#define-a-contract)).
|
|
147
|
+
|
|
148
|
+
## Return images and other media
|
|
149
|
+
|
|
150
|
+
Give the success an `Action.Image` field, the one media kind, and return its bytes and MIME type
|
|
151
|
+
from the handler:
|
|
143
152
|
|
|
144
153
|
```ts
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
success: { printer: Printer, image: Action.Image },
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The tool sends the field as an image block, after the JSON of the rest. Every other surface sends
|
|
158
|
+
it as JSON, the bytes in base64. See
|
|
159
|
+
[mcp-tools/src/printers/Printers.ts](../examples/mcp-tools/src/printers/Printers.ts), and
|
|
160
|
+
[Testing.md](Testing.md#test-mcp-tools) for the client that decodes it back.
|
|
161
|
+
|
|
162
|
+
- An image is a top-level field of a struct success, bare, optional or a required array, or the
|
|
163
|
+
whole success or an array of it. Anywhere else in a served action, as in a `Schema.NullOr`, a
|
|
164
|
+
`Schema.Class` success or an input, `layerHttp` and `runStdio` throw. So an action taking an
|
|
165
|
+
image as input is no tool: leave it out of `actions`
|
|
166
|
+
([kitchen-sink/src/attachments/Attachments.ts](../examples/kitchen-sink/src/attachments/Attachments.ts)).
|
|
167
|
+
- A media field's own `description` reaches no model. Describe the image in the action's
|
|
168
|
+
`description`.
|
|
169
|
+
|
|
170
|
+
## Protect tools
|
|
171
|
+
|
|
172
|
+
Name the protected actions' authentication descriptor and provide its provider to the routes:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const mcp = ActionMcp.layerHttp(implementations, {
|
|
176
|
+
name: "notes-service",
|
|
147
177
|
version: "1.0.0",
|
|
148
|
-
|
|
149
|
-
McpServer.resource({ uri: "docs://readme", name: "README", content: Effect.succeed("# Acme") }),
|
|
150
|
-
McpServer.prompt({ name: "triage", content: () => Effect.succeed("Triage the issue.") }),
|
|
151
|
-
),
|
|
178
|
+
authentication: Login,
|
|
152
179
|
});
|
|
180
|
+
|
|
181
|
+
export const routes = Layer.mergeAll(api, mcp, health, openApi).pipe(Layer.provide(authenticate));
|
|
153
182
|
```
|
|
154
183
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
-
|
|
176
|
-
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
-
|
|
188
|
-
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
- `
|
|
194
|
-
-
|
|
195
|
-
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
-
|
|
208
|
-
-
|
|
209
|
-
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
-
|
|
214
|
-
-
|
|
215
|
-
|
|
216
|
-
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
184
|
+
See [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts). Write the
|
|
185
|
+
[descriptor](Authentication.md#declare-how-callers-sign-in) and its
|
|
186
|
+
[provider](Authentication.md#verify-credentials) once for every surface, and
|
|
187
|
+
[publish discovery](Authentication.md#publish-oauth-discovery) so an MCP client finds where to
|
|
188
|
+
sign in. What a signed-in caller may do is each implementation's
|
|
189
|
+
[`authorize`](Action.md#authorize-calls), which runs on every call of a protected tool.
|
|
190
|
+
|
|
191
|
+
- **One endpoint or two.** An endpoint of protected tools alone authenticates every request,
|
|
192
|
+
the listing included. On a mixed endpoint, serving public and protected tools, a 2026-07-28
|
|
193
|
+
request lists every tool, protected ones included, and calls a public one signed out, and a
|
|
194
|
+
session's every request signs in ([Serve tools over HTTP](#serve-tools-over-http)). A
|
|
195
|
+
protected tool's call answers the 401 a client signs in on. To keep protected tools unlisted
|
|
196
|
+
to signed-out callers, or public ones open to a session's client, serve the public tools on an
|
|
197
|
+
endpoint of their own: a second `layerHttp` on another `path`.
|
|
198
|
+
- No listing is filtered by caller: `authorize` refuses the call, not the sight of the tool.
|
|
199
|
+
- On a Bearer endpoint, a protected tool's `Unauthenticated`, or `Forbidden` naming `scopes`,
|
|
200
|
+
answers the request with 401 or 403 and a challenge the client acts on, in place of a tool
|
|
201
|
+
result ([Authentication.md](Authentication.md#step-up-for-a-scope)). A `Forbidden` naming none
|
|
202
|
+
is an `isError` tool result, in a 200. Once a handler has sent a notification the response has
|
|
203
|
+
started, and its refusal is a tool result: refuse in `authorize`.
|
|
204
|
+
- Test as a signed-in caller ([Testing.md](Testing.md#test-as-a-signed-in-caller)):
|
|
205
|
+
[notes-service/test/routes.test.ts](../examples/notes-service/test/routes.test.ts).
|
|
206
|
+
`Testing.mcpClient` decodes a refusal alike from a 401, a 403 or a tool result: to assert the
|
|
207
|
+
status, send `Testing.mcpRequest`.
|
|
208
|
+
|
|
209
|
+
## Allow browser clients
|
|
210
|
+
|
|
211
|
+
A browser MCP client sends its page's `Origin`, and needs two things.
|
|
212
|
+
|
|
213
|
+
- **`allowedOrigins`.** A request carrying an `Origin` is refused with 403 unless that exact
|
|
214
|
+
origin is listed, so a page a user happens to visit cannot call the tools; one without, as
|
|
215
|
+
other clients send, passes. Read the list from configuration:
|
|
216
|
+
[mcp-tools/src/routes.ts](../examples/mcp-tools/src/routes.ts).
|
|
217
|
+
- **CORS.** `allowedOrigins` only refuses: it answers no preflight and adds no header. Add
|
|
218
|
+
`HttpRouter.cors`, global to the router, only where an origin is configured: given an empty
|
|
219
|
+
list, it answers `Access-Control-Allow-Origin: *`, and given one origin it names that origin
|
|
220
|
+
to every request, an unlisted page's included, which `allowedOrigins` still refuses. Allow the methods `POST` and
|
|
221
|
+
`DELETE`, the endpoint's only ones, and the headers `Content-Type`, `Authorization`,
|
|
222
|
+
`MCP-Protocol-Version`, `MCP-Method`, `MCP-Name` and `Mcp-Session-Id`; expose
|
|
223
|
+
`WWW-Authenticate`, `MCP-Protocol-Version` and `Mcp-Session-Id`: the same `routes.ts`, and
|
|
224
|
+
[kitchen-sink/src/request-policy.ts](../examples/kitchen-sink/src/request-policy.ts) with
|
|
225
|
+
authentication.
|
|
226
|
+
|
|
227
|
+
The endpoint's authentication runs before its Origin check, so `allowedOrigins` keeps no request
|
|
228
|
+
from the verifier. A `Host` allowlist, for a request that sends no `Origin`, or an Origin check
|
|
229
|
+
that must run first is a request policy:
|
|
230
|
+
[Authentication-advanced.md](Authentication-advanced.md#run-a-request-policy-before-authentication).
|
|
231
|
+
|
|
232
|
+
## Add native prompts and resources
|
|
233
|
+
|
|
234
|
+
Merge Effect's own `McpServer.resource`, `McpServer.prompt` and `McpServer.toolkit` layers into
|
|
235
|
+
one and pass it as `features`:
|
|
236
|
+
[kitchen-sink/src/issues/guide.ts](../examples/kitchen-sink/src/issues/guide.ts), given in
|
|
237
|
+
[kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) and
|
|
238
|
+
[kitchen-sink/src/stdio.ts](../examples/kitchen-sink/src/stdio.ts). Provide a service they need
|
|
239
|
+
around the endpoint, never to the `features` layer itself: there it is built again for the
|
|
240
|
+
features alone, so a resource reads another store than the tools write.
|
|
241
|
+
|
|
242
|
+
- Merged beside the endpoint, not given as `features`, they build and are never served.
|
|
243
|
+
- No `authorize` runs for them, and they run with the services they were built with, never a
|
|
244
|
+
request's, so they cannot read the caller. Content that depends on the caller is an action.
|
|
245
|
+
- On an endpoint naming `authentication`, every prompt and resource request needs a credential,
|
|
246
|
+
listings included. To offer them to signed-out callers, serve them on an endpoint without
|
|
247
|
+
protected tools.
|
|
248
|
+
|
|
249
|
+
## Send notifications from a handler
|
|
250
|
+
|
|
251
|
+
A handler yields `McpServer.McpServer` and sends through it:
|
|
252
|
+
`server.notifications["notifications/progress"]({ progressToken, progress })`.
|
|
253
|
+
`McpSchema.McpRequestContext` holds what the client sent: its `clientInfo`, and the request's
|
|
254
|
+
`_meta` as `requestMetadata`. The endpoint and `runStdio` provide both; an implementation also
|
|
255
|
+
served on another surface reads them with `Effect.serviceOption`.
|
|
256
|
+
|
|
257
|
+
- On 2026-07-28, a progress notification is sent only with the token its request carried,
|
|
258
|
+
`requestMetadata?.progressToken`. With another, or when the client sent none, it is dropped
|
|
259
|
+
silently.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# ActionRpc failure modes
|
|
2
|
+
|
|
3
|
+
What you see when something is wrong, and the fix. Search for a fragment of the message.
|
|
4
|
+
The tasks are in [ActionRpc.md](ActionRpc.md).
|
|
5
|
+
|
|
6
|
+
- `No action of these implementations is in this RPC binding: <names>` thrown by `layer`: none of the implementations' actions was passed to this binding's `make`. Implement the contract value the binding received; `(another contract)` marks an equal-looking copy.
|
|
7
|
+
- `Listed in actions, but the binding does not hold it: <names>`, or `but no implementation holds it`, thrown by `layer`: an action in `layer`'s `actions` is not the binding's or the implementations', by identity.
|
|
8
|
+
- `Duplicate served action: <name>` thrown by `layer`: one `layer` call received two implementations of one bound action. Pass one.
|
|
9
|
+
- `ActionRpc binding: error _tag "<tag>" is built in, and declared on every surface` thrown by `make`: the binding's `error` holds a built-in error, or one reusing a built-in tag. Drop the built-in one; rename your own.
|
|
10
|
+
- `Protected action '<name>' requires its matching authentication descriptor` thrown by `make` or `layer` in plain JavaScript, a `No overload matches this call` at `make` in TypeScript: the binding holds a protected action and names no descriptor, or one of another identity. Give `{ authentication: Login }`.
|
|
11
|
+
- `ActionRpc serves a protocol speaking JSON: ...`, a defect when `layer` builds: the host provides another serialization. Provide a JSON one, on the client too.
|
|
12
|
+
- A type error on `middleware` naming `"Layer middleware fails only with the binding's errors and needs no client"`: a middleware declares an error neither the binding nor the built-ins hold, or is `requiredForClient`. Add the error to the binding's `error`, or check in the handler, declaring the error on its actions.
|
|
13
|
+
- `Provider<CurrentMember, "live-rpc.Login">` among the requirements of `ActionRpc.layer`: the layer serves a protected action, and its authentication provider is not provided. Provide it, `Layer.provide(authenticate)`.
|
|
14
|
+
- `Type 'HttpRouter' is not assignable to type 'never'` where the server is launched: an rpc protocol adds its route to the router, so it belongs inside what `HttpRouter.serve` serves, and here it was provided around `HttpRouter.serve`. Provide the protocol to the rpc layer.
|
|
15
|
+
- `HttpRouter` owed by an `ActionRpc.layer` over stdio or a socket server: its authentication provider publishes a `protectedResource`, on a router those protocols lack. Give the provider serving that protocol no `protectedResource`.
|
|
16
|
+
- `Request<"Requires", X>` owed by an `ActionRpc.layer` over stdio or a socket server though no handler yields `X`: the verifier yields `X`, which only a router-mounted protocol's route provides. Read `X` in the verifier's build, or serve the protected actions over HTTP or a WebSocket.
|
|
17
|
+
- `Request<"Requires", X>` in the type of `ActionRpc.layer`, or `X` not assignable to `never` where the server is launched: a handler or the authorizer yields `X`, which nothing provides per request. A startup layer of `X` does not count, nor a middleware in a list not written as a tuple, nor one listed before a middleware requiring `X`: the first listed is innermost. List a middleware providing `X`, or provide `X` with router middleware around the layer. Where `X` is the identity, a middleware requires it on a layer serving a public action: bind the protected actions alone, or read the identity with `Effect.serviceOption`.
|
|
18
|
+
- `Unauthenticated` `A bearer token is required.` from a browser's WebSocket though the page has a token: a browser WebSocket sets no header on its upgrade, so a token meant for the upgrade is never sent. Send the token on each message, with `RpcClient.withHeaders`.
|
|
19
|
+
- A call dies with `Unknown request tag: <name>`: no layer serves the action, for which the client has a method all the same, or two `ActionRpc.layer`s were given one protocol, and only one receives its messages. Serve every rpc of a protocol from one layer; a second layer needs a protocol of its own, on another path.
|
|
20
|
+
- A call dies with `Internal server error`: a defect, an undeclared failure, a middleware that throws, or a success or a declared failure that does not encode. The cause is in the server's logs and error reporters.
|
|
21
|
+
- `RpcClientError` from every call, Effect's own error: the client could not reach the server. Check the client's protocol, URL and path against the server's.
|