@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.
- package/CHANGELOG.md +121 -0
- package/LICENSE +21 -0
- package/README.md +111 -2
- package/dist/cjs/Diagnostics.d.ts +46 -0
- package/dist/cjs/Diagnostics.d.ts.map +1 -0
- package/dist/cjs/Diagnostics.js +29 -0
- package/dist/cjs/Diagnostics.js.map +7 -0
- package/dist/cjs/McpClient.d.ts +304 -0
- package/dist/cjs/McpClient.d.ts.map +1 -0
- package/dist/cjs/McpClient.js +622 -0
- package/dist/cjs/McpClient.js.map +7 -0
- package/dist/cjs/McpError.d.ts +44 -0
- package/dist/cjs/McpError.d.ts.map +1 -0
- package/dist/cjs/McpError.js +41 -0
- package/dist/cjs/McpError.js.map +7 -0
- package/dist/cjs/McpFlows.d.ts +113 -0
- package/dist/cjs/McpFlows.d.ts.map +1 -0
- package/dist/cjs/McpFlows.js +127 -0
- package/dist/cjs/McpFlows.js.map +7 -0
- package/dist/cjs/index.d.ts +39 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +41 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/DiagnosticReporter.d.ts +17 -0
- package/dist/cjs/internal/DiagnosticReporter.d.ts.map +1 -0
- package/dist/cjs/internal/DiagnosticReporter.js +56 -0
- package/dist/cjs/internal/DiagnosticReporter.js.map +7 -0
- package/dist/cjs/internal/HttpTransport.d.ts +67 -0
- package/dist/cjs/internal/HttpTransport.d.ts.map +1 -0
- package/dist/cjs/internal/HttpTransport.js +298 -0
- package/dist/cjs/internal/HttpTransport.js.map +7 -0
- package/dist/cjs/internal/JsonLimits.d.ts +29 -0
- package/dist/cjs/internal/JsonLimits.d.ts.map +1 -0
- package/dist/cjs/internal/JsonLimits.js +51 -0
- package/dist/cjs/internal/JsonLimits.js.map +7 -0
- package/dist/cjs/internal/Limits.d.ts +36 -0
- package/dist/cjs/internal/Limits.d.ts.map +1 -0
- package/dist/cjs/internal/Limits.js +34 -0
- package/dist/cjs/internal/Limits.js.map +7 -0
- package/dist/cjs/internal/Rpc.d.ts +141 -0
- package/dist/cjs/internal/Rpc.d.ts.map +1 -0
- package/dist/cjs/internal/Rpc.js +92 -0
- package/dist/cjs/internal/Rpc.js.map +7 -0
- package/dist/cjs/internal/StdioTransport.d.ts +78 -0
- package/dist/cjs/internal/StdioTransport.d.ts.map +1 -0
- package/dist/cjs/internal/StdioTransport.js +310 -0
- package/dist/cjs/internal/StdioTransport.js.map +7 -0
- package/dist/cjs/internal/Transport.d.ts +87 -0
- package/dist/cjs/internal/Transport.d.ts.map +1 -0
- package/dist/cjs/internal/Transport.js +116 -0
- package/dist/cjs/internal/Transport.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/Diagnostics.d.ts +46 -0
- package/dist/esm/Diagnostics.d.ts.map +1 -0
- package/dist/esm/Diagnostics.js +26 -0
- package/dist/esm/Diagnostics.js.map +1 -0
- package/dist/esm/McpClient.d.ts +304 -0
- package/dist/esm/McpClient.d.ts.map +1 -0
- package/dist/esm/McpClient.js +671 -0
- package/dist/esm/McpClient.js.map +1 -0
- package/dist/esm/McpError.d.ts +44 -0
- package/dist/esm/McpError.d.ts.map +1 -0
- package/dist/esm/McpError.js +43 -0
- package/dist/esm/McpError.js.map +1 -0
- package/dist/esm/McpFlows.d.ts +113 -0
- package/dist/esm/McpFlows.d.ts.map +1 -0
- package/dist/esm/McpFlows.js +168 -0
- package/dist/esm/McpFlows.js.map +1 -0
- package/dist/esm/index.d.ts +39 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +39 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/DiagnosticReporter.d.ts +17 -0
- package/dist/esm/internal/DiagnosticReporter.d.ts.map +1 -0
- package/dist/esm/internal/DiagnosticReporter.js +44 -0
- package/dist/esm/internal/DiagnosticReporter.js.map +1 -0
- package/dist/esm/internal/HttpTransport.d.ts +67 -0
- package/dist/esm/internal/HttpTransport.d.ts.map +1 -0
- package/dist/esm/internal/HttpTransport.js +266 -0
- package/dist/esm/internal/HttpTransport.js.map +1 -0
- package/dist/esm/internal/JsonLimits.d.ts +29 -0
- package/dist/esm/internal/JsonLimits.d.ts.map +1 -0
- package/dist/esm/internal/JsonLimits.js +55 -0
- package/dist/esm/internal/JsonLimits.js.map +1 -0
- package/dist/esm/internal/Limits.d.ts +36 -0
- package/dist/esm/internal/Limits.d.ts.map +1 -0
- package/dist/esm/internal/Limits.js +41 -0
- package/dist/esm/internal/Limits.js.map +1 -0
- package/dist/esm/internal/Rpc.d.ts +141 -0
- package/dist/esm/internal/Rpc.d.ts.map +1 -0
- package/dist/esm/internal/Rpc.js +129 -0
- package/dist/esm/internal/Rpc.js.map +1 -0
- package/dist/esm/internal/StdioTransport.d.ts +78 -0
- package/dist/esm/internal/StdioTransport.d.ts.map +1 -0
- package/dist/esm/internal/StdioTransport.js +332 -0
- package/dist/esm/internal/StdioTransport.js.map +1 -0
- package/dist/esm/internal/Transport.d.ts +87 -0
- package/dist/esm/internal/Transport.d.ts.map +1 -0
- package/dist/esm/internal/Transport.js +146 -0
- package/dist/esm/internal/Transport.js.map +1 -0
- package/docs/README.md +139 -0
- package/docs/api.md +469 -0
- package/docs/concepts/the-session.md +135 -0
- package/docs/concepts/tools-as-flows.md +116 -0
- package/docs/guides/bound-an-untrusted-server.md +158 -0
- package/docs/guides/configure-servers-for-the-cli.md +167 -0
- package/docs/guides/connect-a-server.md +161 -0
- package/docs/guides/grant-authority-to-mcp-tools.md +130 -0
- package/docs/guides/handle-a-failed-tool-call.md +125 -0
- package/docs/guides/select-the-tools-a-run-sees.md +92 -0
- package/docs/guides/testing.md +132 -0
- package/docs/guides/validate-structured-output.md +103 -0
- package/docs/installation.md +117 -0
- package/docs/quickstart.md +200 -0
- package/docs/troubleshooting.md +316 -0
- package/package.json +157 -3
- package/src/Diagnostics.ts +47 -0
- package/src/McpClient.ts +985 -0
- package/src/McpError.ts +52 -0
- package/src/McpFlows.ts +211 -0
- package/src/index.ts +42 -0
- package/src/internal/DiagnosticReporter.ts +47 -0
- package/src/internal/HttpTransport.ts +400 -0
- package/src/internal/JsonLimits.ts +53 -0
- package/src/internal/Limits.ts +48 -0
- package/src/internal/Rpc.ts +219 -0
- package/src/internal/StdioTransport.ts +491 -0
- package/src/internal/Transport.ts +178 -0
package/src/McpError.ts
ADDED
|
@@ -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
|
+
}) {}
|
package/src/McpFlows.ts
ADDED
|
@@ -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
|
+
)
|