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

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 +112 -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 +112 -0
  66. package/dist/esm/McpFlows.d.ts.map +1 -0
  67. package/dist/esm/McpFlows.js +167 -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 +210 -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,210 @@
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 because every binding
60
+ * shares this reference.
61
+ *
62
+ * @category constants
63
+ * @since 1.0.0-rc.0
64
+ */
65
+ export const capabilities: ReadonlyArray<string> = Object.freeze(
66
+ Capability.Action.literals.map((action) => `${action}:**`)
67
+ )
68
+
69
+ /**
70
+ * The conservative effect envelope every MCP tool flow declares.
71
+ *
72
+ * @category effects
73
+ * @since 1.0.0-rc.0
74
+ */
75
+ export const effects = Effects.make({
76
+ reads: ["**"],
77
+ writes: ["**"],
78
+ mode: "expected",
79
+ onConflict: "serialize",
80
+ tier: "irreversible"
81
+ })
82
+
83
+ /**
84
+ * Selects and names the tools projected from one MCP catalog.
85
+ *
86
+ * @category models
87
+ * @since 1.0.0-rc.0
88
+ */
89
+ export interface ProjectionOptions {
90
+ /** Exact tool names to project. Omitted or empty means every tool. */
91
+ readonly include?: ReadonlyArray<string> | undefined
92
+ /** Exact tool names to drop, applied after `include`. */
93
+ readonly exclude?: ReadonlyArray<string> | undefined
94
+ /** Replaces the default `mcp/<server>` flow-name prefix. */
95
+ readonly namePrefix?: string | undefined
96
+ }
97
+
98
+ /** One remote tool, bound to a flow name scoped by server so two servers may reuse a tool name. */
99
+ const toolBinding = (
100
+ client: McpClient.McpClient,
101
+ tool: McpClient.ToolDescription,
102
+ prefix: string
103
+ ): FlowBinding.Binding => {
104
+ const toolName = tool.name
105
+ return FlowBinding.make({
106
+ flow: {
107
+ name: `${prefix}/${toolName}`,
108
+ // The server authors this text, so it is attributed as remote content
109
+ // rather than read as if the host had written it. The client bounds it.
110
+ description: tool.description === undefined
111
+ ? `MCP tool "${toolName}" on server "${client.server}"`
112
+ : `MCP tool "${toolName}" on server "${client.server}". Server-supplied description: ${tool.description}`,
113
+ capabilities,
114
+ effects,
115
+ input: Args,
116
+ output: Result
117
+ },
118
+ // The server's own JSON Schema document, carried by value so a caller
119
+ // reading `ctx.flows` sees the real parameter shape rather than `Args`'s
120
+ // permissive record type.
121
+ inputDocument: tool.inputSchema as Schema.Json,
122
+ // McpClient authors these messages with remote/process diagnostics withheld.
123
+ publicError: (error: McpError) => error.message,
124
+ handler: (input): Effect.Effect<typeof Result.Type, McpError> =>
125
+ Effect.map(client.callTool(toolName, input), (result) =>
126
+ result.structuredContent === undefined
127
+ ? { content: result.content, isError: result.isError }
128
+ : {
129
+ content: result.content,
130
+ isError: result.isError,
131
+ structuredContent: result.structuredContent
132
+ })
133
+ })
134
+ }
135
+
136
+ /**
137
+ * Projects an already-connected MCP session's tool catalog as a
138
+ * {@link FlowBinding.Source}, one flow per tool.
139
+ *
140
+ * The client is a precondition, not a parameter this constructor resolves:
141
+ * connecting is a scoped effect (it owns a subprocess), and a `Source` is not
142
+ * scoped, so the host composes {@link McpClient.connect} once, at the same
143
+ * place it composes every other scoped kernel service, and passes the live
144
+ * client here.
145
+ *
146
+ * This constructor is total: it applies exact filters to the catalog it is
147
+ * given. Use {@link connected} when unknown `include` names and an empty
148
+ * `namePrefix` must fail loudly against a freshly fetched catalog.
149
+ *
150
+ * @category constructors
151
+ * @since 1.0.0-rc.0
152
+ */
153
+ export const mcp = (client: McpClient.McpClient, options: ProjectionOptions = {}): FlowBinding.Source => {
154
+ const prefix = options.namePrefix ?? `mcp/${client.server}`
155
+ const include = options.include === undefined || options.include.length === 0
156
+ ? undefined
157
+ : new Set(options.include)
158
+ const exclude = new Set(options.exclude ?? [])
159
+ const tools = client.tools.filter((tool) =>
160
+ (include === undefined || include.has(tool.name)) && !exclude.has(tool.name)
161
+ )
162
+ return FlowBinding.source(prefix, tools.map((tool) => toolBinding(client, tool, prefix)))
163
+ }
164
+
165
+ /**
166
+ * Connects to an MCP server and projects its tools in one step.
167
+ *
168
+ * This is the checked entry point: it validates projection options against
169
+ * the freshly fetched catalog before constructing the source. An empty prefix
170
+ * fails before spawning. Failed or interrupted setup, including projection
171
+ * validation, releases the connection before returning to the caller.
172
+ *
173
+ * @category constructors
174
+ * @since 1.0.0-rc.0
175
+ */
176
+ export const connected = <O extends McpClient.ConnectOptions>(
177
+ options: O & ProjectionOptions
178
+ ): Effect.Effect<FlowBinding.Source, McpError, McpClient.Requirements<O> | Scope.Scope> =>
179
+ Effect.gen(function*() {
180
+ if (options.namePrefix === "") {
181
+ return yield* Effect.fail(
182
+ new McpError({
183
+ code: "protocol_error",
184
+ message: `MCP server "${options.server}" option "namePrefix" must not be empty`,
185
+ server: options.server
186
+ })
187
+ )
188
+ }
189
+ return yield* Effect.acquireUseRelease(
190
+ Effect.flatMap(Effect.scope, Scope.fork),
191
+ (scope) =>
192
+ Effect.gen(function*() {
193
+ const client = yield* McpClient.connect(options)
194
+ const offered = new Set(client.tools.map((tool) => tool.name))
195
+ const missing = options.include?.find((name) => !offered.has(name))
196
+ if (missing !== undefined) {
197
+ return yield* Effect.fail(
198
+ new McpError({
199
+ code: "tool_not_found",
200
+ message: `MCP server "${client.server}" offers no requested include tool`,
201
+ server: client.server
202
+ })
203
+ )
204
+ }
205
+ return mcp(client, options)
206
+ }).pipe(Scope.provide(scope)),
207
+ // Projection rejection must release the successfully initialized client.
208
+ (scope, exit) => Exit.isFailure(exit) ? Scope.close(scope, exit) : Effect.void
209
+ )
210
+ })
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
+ )