@smthrs/mcp 0.0.0-stage → 1.0.0-rc.4

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 (128) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/LICENSE +21 -0
  3. package/README.md +111 -2
  4. package/dist/cjs/Diagnostics.d.ts +46 -0
  5. package/dist/cjs/Diagnostics.d.ts.map +1 -0
  6. package/dist/cjs/Diagnostics.js +29 -0
  7. package/dist/cjs/Diagnostics.js.map +7 -0
  8. package/dist/cjs/McpClient.d.ts +304 -0
  9. package/dist/cjs/McpClient.d.ts.map +1 -0
  10. package/dist/cjs/McpClient.js +622 -0
  11. package/dist/cjs/McpClient.js.map +7 -0
  12. package/dist/cjs/McpError.d.ts +44 -0
  13. package/dist/cjs/McpError.d.ts.map +1 -0
  14. package/dist/cjs/McpError.js +41 -0
  15. package/dist/cjs/McpError.js.map +7 -0
  16. package/dist/cjs/McpFlows.d.ts +113 -0
  17. package/dist/cjs/McpFlows.d.ts.map +1 -0
  18. package/dist/cjs/McpFlows.js +127 -0
  19. package/dist/cjs/McpFlows.js.map +7 -0
  20. package/dist/cjs/index.d.ts +39 -0
  21. package/dist/cjs/index.d.ts.map +1 -0
  22. package/dist/cjs/index.js +41 -0
  23. package/dist/cjs/index.js.map +7 -0
  24. package/dist/cjs/internal/DiagnosticReporter.d.ts +17 -0
  25. package/dist/cjs/internal/DiagnosticReporter.d.ts.map +1 -0
  26. package/dist/cjs/internal/DiagnosticReporter.js +56 -0
  27. package/dist/cjs/internal/DiagnosticReporter.js.map +7 -0
  28. package/dist/cjs/internal/HttpTransport.d.ts +67 -0
  29. package/dist/cjs/internal/HttpTransport.d.ts.map +1 -0
  30. package/dist/cjs/internal/HttpTransport.js +298 -0
  31. package/dist/cjs/internal/HttpTransport.js.map +7 -0
  32. package/dist/cjs/internal/JsonLimits.d.ts +29 -0
  33. package/dist/cjs/internal/JsonLimits.d.ts.map +1 -0
  34. package/dist/cjs/internal/JsonLimits.js +51 -0
  35. package/dist/cjs/internal/JsonLimits.js.map +7 -0
  36. package/dist/cjs/internal/Limits.d.ts +36 -0
  37. package/dist/cjs/internal/Limits.d.ts.map +1 -0
  38. package/dist/cjs/internal/Limits.js +34 -0
  39. package/dist/cjs/internal/Limits.js.map +7 -0
  40. package/dist/cjs/internal/Rpc.d.ts +141 -0
  41. package/dist/cjs/internal/Rpc.d.ts.map +1 -0
  42. package/dist/cjs/internal/Rpc.js +92 -0
  43. package/dist/cjs/internal/Rpc.js.map +7 -0
  44. package/dist/cjs/internal/StdioTransport.d.ts +78 -0
  45. package/dist/cjs/internal/StdioTransport.d.ts.map +1 -0
  46. package/dist/cjs/internal/StdioTransport.js +310 -0
  47. package/dist/cjs/internal/StdioTransport.js.map +7 -0
  48. package/dist/cjs/internal/Transport.d.ts +87 -0
  49. package/dist/cjs/internal/Transport.d.ts.map +1 -0
  50. package/dist/cjs/internal/Transport.js +116 -0
  51. package/dist/cjs/internal/Transport.js.map +7 -0
  52. package/dist/cjs/package.json +1 -0
  53. package/dist/esm/Diagnostics.d.ts +46 -0
  54. package/dist/esm/Diagnostics.d.ts.map +1 -0
  55. package/dist/esm/Diagnostics.js +26 -0
  56. package/dist/esm/Diagnostics.js.map +1 -0
  57. package/dist/esm/McpClient.d.ts +304 -0
  58. package/dist/esm/McpClient.d.ts.map +1 -0
  59. package/dist/esm/McpClient.js +671 -0
  60. package/dist/esm/McpClient.js.map +1 -0
  61. package/dist/esm/McpError.d.ts +44 -0
  62. package/dist/esm/McpError.d.ts.map +1 -0
  63. package/dist/esm/McpError.js +43 -0
  64. package/dist/esm/McpError.js.map +1 -0
  65. package/dist/esm/McpFlows.d.ts +113 -0
  66. package/dist/esm/McpFlows.d.ts.map +1 -0
  67. package/dist/esm/McpFlows.js +168 -0
  68. package/dist/esm/McpFlows.js.map +1 -0
  69. package/dist/esm/index.d.ts +39 -0
  70. package/dist/esm/index.d.ts.map +1 -0
  71. package/dist/esm/index.js +39 -0
  72. package/dist/esm/index.js.map +1 -0
  73. package/dist/esm/internal/DiagnosticReporter.d.ts +17 -0
  74. package/dist/esm/internal/DiagnosticReporter.d.ts.map +1 -0
  75. package/dist/esm/internal/DiagnosticReporter.js +44 -0
  76. package/dist/esm/internal/DiagnosticReporter.js.map +1 -0
  77. package/dist/esm/internal/HttpTransport.d.ts +67 -0
  78. package/dist/esm/internal/HttpTransport.d.ts.map +1 -0
  79. package/dist/esm/internal/HttpTransport.js +266 -0
  80. package/dist/esm/internal/HttpTransport.js.map +1 -0
  81. package/dist/esm/internal/JsonLimits.d.ts +29 -0
  82. package/dist/esm/internal/JsonLimits.d.ts.map +1 -0
  83. package/dist/esm/internal/JsonLimits.js +55 -0
  84. package/dist/esm/internal/JsonLimits.js.map +1 -0
  85. package/dist/esm/internal/Limits.d.ts +36 -0
  86. package/dist/esm/internal/Limits.d.ts.map +1 -0
  87. package/dist/esm/internal/Limits.js +41 -0
  88. package/dist/esm/internal/Limits.js.map +1 -0
  89. package/dist/esm/internal/Rpc.d.ts +141 -0
  90. package/dist/esm/internal/Rpc.d.ts.map +1 -0
  91. package/dist/esm/internal/Rpc.js +129 -0
  92. package/dist/esm/internal/Rpc.js.map +1 -0
  93. package/dist/esm/internal/StdioTransport.d.ts +78 -0
  94. package/dist/esm/internal/StdioTransport.d.ts.map +1 -0
  95. package/dist/esm/internal/StdioTransport.js +332 -0
  96. package/dist/esm/internal/StdioTransport.js.map +1 -0
  97. package/dist/esm/internal/Transport.d.ts +87 -0
  98. package/dist/esm/internal/Transport.d.ts.map +1 -0
  99. package/dist/esm/internal/Transport.js +146 -0
  100. package/dist/esm/internal/Transport.js.map +1 -0
  101. package/docs/README.md +139 -0
  102. package/docs/api.md +469 -0
  103. package/docs/concepts/the-session.md +135 -0
  104. package/docs/concepts/tools-as-flows.md +116 -0
  105. package/docs/guides/bound-an-untrusted-server.md +158 -0
  106. package/docs/guides/configure-servers-for-the-cli.md +167 -0
  107. package/docs/guides/connect-a-server.md +161 -0
  108. package/docs/guides/grant-authority-to-mcp-tools.md +130 -0
  109. package/docs/guides/handle-a-failed-tool-call.md +125 -0
  110. package/docs/guides/select-the-tools-a-run-sees.md +92 -0
  111. package/docs/guides/testing.md +132 -0
  112. package/docs/guides/validate-structured-output.md +103 -0
  113. package/docs/installation.md +117 -0
  114. package/docs/quickstart.md +200 -0
  115. package/docs/troubleshooting.md +316 -0
  116. package/package.json +157 -3
  117. package/src/Diagnostics.ts +47 -0
  118. package/src/McpClient.ts +985 -0
  119. package/src/McpError.ts +52 -0
  120. package/src/McpFlows.ts +211 -0
  121. package/src/index.ts +42 -0
  122. package/src/internal/DiagnosticReporter.ts +47 -0
  123. package/src/internal/HttpTransport.ts +400 -0
  124. package/src/internal/JsonLimits.ts +53 -0
  125. package/src/internal/Limits.ts +48 -0
  126. package/src/internal/Rpc.ts +219 -0
  127. package/src/internal/StdioTransport.ts +491 -0
  128. package/src/internal/Transport.ts +178 -0
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The single typed error returned by the MCP client and flow adapter.
3
+ *
4
+ * @since 1.0.0-rc.0
5
+ */
6
+
7
+ import { Schema } from "effect"
8
+
9
+ /**
10
+ * Stable, model-facing failure codes for MCP operations.
11
+ *
12
+ * @category models
13
+ * @since 1.0.0-rc.0
14
+ */
15
+ export const Code = Schema.Literals([
16
+ "spawn_failed",
17
+ "connection_closed",
18
+ "timeout",
19
+ "protocol_error",
20
+ "tool_not_found",
21
+ "tool_failed",
22
+ "invalid_response"
23
+ ])
24
+
25
+ /**
26
+ * Stable, model-facing failure codes for MCP operations.
27
+ *
28
+ * @category models
29
+ * @since 1.0.0-rc.0
30
+ */
31
+ export type Code = typeof Code.Type
32
+
33
+ /**
34
+ * A recoverable MCP client or flow-adapter failure.
35
+ *
36
+ * Handlers keep ordinary tool outcomes (the remote tool's own `isError`
37
+ * result) in the success channel; this error is reserved for failures of the
38
+ * MCP session itself: the server would not exist, the pipe closed, a
39
+ * response could not be parsed.
40
+ *
41
+ * Client-created messages withhold raw process/remote diagnostics and
42
+ * user-controlled property paths. Trusted hosts can opt into Diagnostics for
43
+ * bounded Redacted details. Successful tool output is not scrubbed.
44
+ *
45
+ * @category errors
46
+ * @since 1.0.0-rc.0
47
+ */
48
+ export class McpError extends Schema.TaggedError<McpError>()("flows/mcp/McpError", {
49
+ code: Code,
50
+ message: Schema.String,
51
+ server: Schema.optional(Schema.String)
52
+ }) {}
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Projects a connected MCP server's tools as an ordinary {@link FlowBinding.Source}.
3
+ *
4
+ * This is the whole adapter: `@smthrs/harness/FlowBinding`'s own module doc
5
+ * already names the target directly: "a standard filesystem flow, a memory
6
+ * flow, an incoming MCP tool, a durable child agent" are all just a flow
7
+ * declaration plus the code that runs it. Nothing about the harness, the
8
+ * registry, or the cell loop needs to know a given flow's implementation
9
+ * happens to proxy a remote MCP `tools/call`; a cell that reads a file and a
10
+ * cell that calls an MCP tool run the identical two lines.
11
+ *
12
+ * @since 1.0.0-rc.0
13
+ */
14
+
15
+ import * as Capability from "@smthrs/capability/Capability"
16
+ import * as Effects from "@smthrs/core/Effects"
17
+ import * as FlowBinding from "@smthrs/harness/FlowBinding"
18
+ import { Effect, Exit, Schema, Scope } from "effect"
19
+ import * as McpClient from "./McpClient.ts"
20
+ import { McpError } from "./McpError.ts"
21
+
22
+ /**
23
+ * Decoded input accepted by every MCP tool flow, which is whatever the remote
24
+ * tool's own JSON Schema describes. The registry still discloses the real parameter
25
+ * shape, as shown by {@link toolBinding}. This is only the runtime decode, and it
26
+ * is permissive because the server, not this adapter, owns validation.
27
+ *
28
+ * @category schemas
29
+ * @since 1.0.0-rc.0
30
+ */
31
+ export const Args = Schema.Record(Schema.String, Schema.Unknown)
32
+
33
+ /**
34
+ * Decoded output returned by every MCP tool flow, carrying the tool's content
35
+ * blocks by shape plus whether the server reported an error.
36
+ *
37
+ * @category schemas
38
+ * @since 1.0.0-rc.0
39
+ */
40
+ export const Result = Schema.Struct({
41
+ content: Schema.Array(Schema.Record(Schema.String, Schema.Unknown)),
42
+ isError: Schema.Boolean,
43
+ structuredContent: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
44
+ })
45
+
46
+ /**
47
+ * The authority every MCP tool flow declares.
48
+ *
49
+ * An MCP tool is opaque code this adapter does not control, the same
50
+ * situation `@smthrs/registry/MarkdownFlow` calls "unprojectable authority"
51
+ * for a skill with no declared `capabilities`: the honest declaration is
52
+ * everything, not a guess.
53
+ *
54
+ * It is spelled as one exact `namespace:operation:resource` per action because
55
+ * the cell boundary reads each declaration with `Capability.parse`, which
56
+ * rejects anything that does not have exactly those three colon-separated
57
+ * components. An unparseable declaration counts as unauthorized. The list is
58
+ * derived from `Capability.Action.literals`, so adding a host action cannot
59
+ * silently omit it from MCP tools, and it is frozen so no caller can narrow
60
+ * or widen what every tool declares. Each binding records it deduplicated and
61
+ * sorted, as `FlowBinding.make` records every declaration.
62
+ *
63
+ * @category constants
64
+ * @since 1.0.0-rc.0
65
+ */
66
+ export const capabilities: ReadonlyArray<string> = Object.freeze(
67
+ Capability.Action.literals.map((action) => `${action}:**`)
68
+ )
69
+
70
+ /**
71
+ * The conservative effect envelope every MCP tool flow declares.
72
+ *
73
+ * @category effects
74
+ * @since 1.0.0-rc.0
75
+ */
76
+ export const effects = Effects.make({
77
+ reads: ["**"],
78
+ writes: ["**"],
79
+ mode: "expected",
80
+ onConflict: "serialize",
81
+ tier: "irreversible"
82
+ })
83
+
84
+ /**
85
+ * Selects and names the tools projected from one MCP catalog.
86
+ *
87
+ * @category models
88
+ * @since 1.0.0-rc.0
89
+ */
90
+ export interface ProjectionOptions {
91
+ /** Exact tool names to project. Omitted or empty means every tool. */
92
+ readonly include?: ReadonlyArray<string> | undefined
93
+ /** Exact tool names to drop, applied after `include`. */
94
+ readonly exclude?: ReadonlyArray<string> | undefined
95
+ /** Replaces the default `mcp/<server>` flow-name prefix. */
96
+ readonly namePrefix?: string | undefined
97
+ }
98
+
99
+ /** One remote tool, bound to a flow name scoped by server so two servers may reuse a tool name. */
100
+ const toolBinding = (
101
+ client: McpClient.McpClient,
102
+ tool: McpClient.ToolDescription,
103
+ prefix: string
104
+ ): FlowBinding.Binding => {
105
+ const toolName = tool.name
106
+ return FlowBinding.make({
107
+ flow: {
108
+ name: `${prefix}/${toolName}`,
109
+ // The server authors this text, so it is attributed as remote content
110
+ // rather than read as if the host had written it. The client bounds it.
111
+ description: tool.description === undefined
112
+ ? `MCP tool "${toolName}" on server "${client.server}"`
113
+ : `MCP tool "${toolName}" on server "${client.server}". Server-supplied description: ${tool.description}`,
114
+ capabilities,
115
+ effects,
116
+ input: Args,
117
+ output: Result
118
+ },
119
+ // The server's own JSON Schema document, carried by value so a caller
120
+ // reading `ctx.flows` sees the real parameter shape rather than `Args`'s
121
+ // permissive record type.
122
+ inputDocument: tool.inputSchema as Schema.Json,
123
+ // McpClient authors these messages with remote/process diagnostics withheld.
124
+ publicError: (error: McpError) => error.message,
125
+ handler: (input): Effect.Effect<typeof Result.Type, McpError> =>
126
+ Effect.map(client.callTool(toolName, input), (result) =>
127
+ result.structuredContent === undefined
128
+ ? { content: result.content, isError: result.isError }
129
+ : {
130
+ content: result.content,
131
+ isError: result.isError,
132
+ structuredContent: result.structuredContent
133
+ })
134
+ })
135
+ }
136
+
137
+ /**
138
+ * Projects an already-connected MCP session's tool catalog as a
139
+ * {@link FlowBinding.Source}, one flow per tool.
140
+ *
141
+ * The client is a precondition, not a parameter this constructor resolves:
142
+ * connecting is a scoped effect (it owns a subprocess), and a `Source` is not
143
+ * scoped, so the host composes {@link McpClient.connect} once, at the same
144
+ * place it composes every other scoped kernel service, and passes the live
145
+ * client here.
146
+ *
147
+ * This constructor is total: it applies exact filters to the catalog it is
148
+ * given. Use {@link connected} when unknown `include` names and an empty
149
+ * `namePrefix` must fail loudly against a freshly fetched catalog.
150
+ *
151
+ * @category constructors
152
+ * @since 1.0.0-rc.0
153
+ */
154
+ export const mcp = (client: McpClient.McpClient, options: ProjectionOptions = {}): FlowBinding.Source => {
155
+ const prefix = options.namePrefix ?? `mcp/${client.server}`
156
+ const include = options.include === undefined || options.include.length === 0
157
+ ? undefined
158
+ : new Set(options.include)
159
+ const exclude = new Set(options.exclude ?? [])
160
+ const tools = client.tools.filter((tool) =>
161
+ (include === undefined || include.has(tool.name)) && !exclude.has(tool.name)
162
+ )
163
+ return FlowBinding.source(prefix, tools.map((tool) => toolBinding(client, tool, prefix)))
164
+ }
165
+
166
+ /**
167
+ * Connects to an MCP server and projects its tools in one step.
168
+ *
169
+ * This is the checked entry point: it validates projection options against
170
+ * the freshly fetched catalog before constructing the source. An empty prefix
171
+ * fails before spawning. Failed or interrupted setup, including projection
172
+ * validation, releases the connection before returning to the caller.
173
+ *
174
+ * @category constructors
175
+ * @since 1.0.0-rc.0
176
+ */
177
+ export const connected = <O extends McpClient.ConnectOptions>(
178
+ options: O & ProjectionOptions
179
+ ): Effect.Effect<FlowBinding.Source, McpError, McpClient.Requirements<O> | Scope.Scope> =>
180
+ Effect.gen(function*() {
181
+ if (options.namePrefix === "") {
182
+ return yield* Effect.fail(
183
+ new McpError({
184
+ code: "protocol_error",
185
+ message: `MCP server "${options.server}" option "namePrefix" must not be empty`,
186
+ server: options.server
187
+ })
188
+ )
189
+ }
190
+ return yield* Effect.acquireUseRelease(
191
+ Effect.flatMap(Effect.scope, Scope.fork),
192
+ (scope) =>
193
+ Effect.gen(function*() {
194
+ const client = yield* McpClient.connect(options)
195
+ const offered = new Set(client.tools.map((tool) => tool.name))
196
+ const missing = options.include?.find((name) => !offered.has(name))
197
+ if (missing !== undefined) {
198
+ return yield* Effect.fail(
199
+ new McpError({
200
+ code: "tool_not_found",
201
+ message: `MCP server "${client.server}" offers no requested include tool`,
202
+ server: client.server
203
+ })
204
+ )
205
+ }
206
+ return mcp(client, options)
207
+ }).pipe(Scope.provide(scope)),
208
+ // Projection rejection must release the successfully initialized client.
209
+ (scope, exit) => Exit.isFailure(exit) ? Scope.close(scope, exit) : Effect.void
210
+ )
211
+ })
package/src/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Model Context Protocol client and flow bindings.
3
+ *
4
+ * `McpClient` speaks the stdio protocol to a configured MCP server, and
5
+ * `McpFlows` projects that server's tools into the flow catalog as
6
+ * `mcp/<server>/<tool>` bindings. `@smthrs/cli` composes both behind
7
+ * `--mcp-config`.
8
+ *
9
+ * ```ts
10
+ * import { McpFlows } from "@smthrs/mcp"
11
+ * ```
12
+ *
13
+ * @since 1.0.0-rc.0
14
+ */
15
+
16
+ /**
17
+ * @category services
18
+ * @since 1.0.0-rc.0
19
+ * @slop
20
+ */
21
+ export * as McpClient from "./McpClient.ts"
22
+
23
+ /**
24
+ * @category services
25
+ * @since 1.0.0-rc.0
26
+ * @slop
27
+ */
28
+ export * as Diagnostics from "./Diagnostics.ts"
29
+
30
+ /**
31
+ * @category errors
32
+ * @since 1.0.0-rc.0
33
+ * @slop
34
+ */
35
+ export * as McpError from "./McpError.ts"
36
+
37
+ /**
38
+ * @category services
39
+ * @since 1.0.0-rc.0
40
+ * @slop
41
+ */
42
+ export * as McpFlows from "./McpFlows.ts"
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Internal optional-observer capture. Every detail is redacted with
3
+ * `Redaction.redactDiagnostic` and then wrapped in `Redacted`. Stderr is redacted by the
4
+ * transport, one whole line at a time, before its byte cap.
5
+ *
6
+ * @since 1.0.0-rc.0
7
+ */
8
+
9
+ import * as Redaction from "@smthrs/journal/Redaction"
10
+ import { Effect, Option, Redacted } from "effect"
11
+ import * as Diagnostics from "../Diagnostics.ts"
12
+
13
+ /**
14
+ * Captures the optional host observer without introducing a required service.
15
+ *
16
+ * @category constructors
17
+ * @since 1.0.0-rc.0
18
+ */
19
+ export const make = (server: string) =>
20
+ Effect.map(
21
+ Effect.serviceOption(Diagnostics.Diagnostics),
22
+ (observer) => (source: Diagnostics.Event["source"], detail: unknown): void => {
23
+ if (Option.isNone(observer)) return
24
+ try {
25
+ // A remote error can echo a credential and an argument snapshot can
26
+ // carry one under a secret key, so every source is redacted here.
27
+ // StdioTransport redacts stderr line by line as it arrives, holding a
28
+ // value that spans lines until it closes, and caps only the redacted
29
+ // text to `maxStderrBytes`, so the cap cannot cut a credential's
30
+ // recognizable prefix. Redacting that capped tail again would regrow
31
+ // it past the cap.
32
+ const redacted = source === "stderr" ? detail : Redaction.redactDiagnostic(detail)
33
+ const text = typeof redacted === "string" ? redacted : JSON.stringify(redacted)
34
+ const bytes = new TextEncoder().encode(text)
35
+ const truncated = bytes.byteLength > 16_384
36
+ observer.value.report({
37
+ server,
38
+ source,
39
+ detail: Redacted.make(new TextDecoder().decode(bytes.subarray(0, 16_384), { stream: truncated })),
40
+ truncated
41
+ })
42
+ } catch {
43
+ // Observer failures are not MCP failures. Their messages can themselves
44
+ // contain the diagnostic, so neither attach nor log the thrown value.
45
+ }
46
+ }
47
+ )