@effect-uai/mcp 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/Client.d.mts +55 -0
- package/dist/Client.d.mts.map +1 -0
- package/dist/Client.mjs +85 -0
- package/dist/Client.mjs.map +1 -0
- package/dist/McpError-DWg9BlW5.d.mts +83 -0
- package/dist/McpError-DWg9BlW5.d.mts.map +1 -0
- package/dist/McpError.d.mts +2 -0
- package/dist/McpError.mjs +61 -0
- package/dist/McpError.mjs.map +1 -0
- package/dist/Toolkit.d.mts +20 -0
- package/dist/Toolkit.d.mts.map +1 -0
- package/dist/Toolkit.mjs +70 -0
- package/dist/Toolkit.mjs.map +1 -0
- package/dist/auth-ZDvsxpCo.d.mts +126 -0
- package/dist/auth-ZDvsxpCo.d.mts.map +1 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +4 -0
- package/dist/internal/auth.d.mts +2 -0
- package/dist/internal/auth.mjs +25 -0
- package/dist/internal/auth.mjs.map +1 -0
- package/dist/internal/httpTransport.d.mts +17 -0
- package/dist/internal/httpTransport.d.mts.map +1 -0
- package/dist/internal/httpTransport.mjs +88 -0
- package/dist/internal/httpTransport.mjs.map +1 -0
- package/dist/internal/protocol.d.mts +2 -0
- package/dist/internal/protocol.mjs +23 -0
- package/dist/internal/protocol.mjs.map +1 -0
- package/dist/internal/protocols/2025-06-18.d.mts +11 -0
- package/dist/internal/protocols/2025-06-18.d.mts.map +1 -0
- package/dist/internal/protocols/2025-06-18.mjs +70 -0
- package/dist/internal/protocols/2025-06-18.mjs.map +1 -0
- package/dist/internal/protocols/2025-11-25.d.mts +2 -0
- package/dist/internal/protocols/2025-11-25.mjs +2 -0
- package/dist/internal/protocols/2026-07-28.d.mts +16 -0
- package/dist/internal/protocols/2026-07-28.d.mts.map +1 -0
- package/dist/internal/protocols/2026-07-28.mjs +83 -0
- package/dist/internal/protocols/2026-07-28.mjs.map +1 -0
- package/dist/internal/rpc.d.mts +2 -0
- package/dist/internal/rpc.mjs +78 -0
- package/dist/internal/rpc.mjs.map +1 -0
- package/dist/internal/schema.d.mts +2 -0
- package/dist/internal/schema.mjs +168 -0
- package/dist/internal/schema.mjs.map +1 -0
- package/dist/internal/stdioTransport.d.mts +15 -0
- package/dist/internal/stdioTransport.d.mts.map +1 -0
- package/dist/internal/stdioTransport.mjs +46 -0
- package/dist/internal/stdioTransport.mjs.map +1 -0
- package/dist/protocol-kjC3iAMb.d.mts +35 -0
- package/dist/protocol-kjC3iAMb.d.mts.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/rpc-Bq95Lio9.d.mts +34 -0
- package/dist/rpc-Bq95Lio9.d.mts.map +1 -0
- package/dist/schema-D2oUNuo5.d.mts +245 -0
- package/dist/schema-D2oUNuo5.d.mts.map +1 -0
- package/package.json +70 -0
- package/src/Client.ts +180 -0
- package/src/McpError.ts +93 -0
- package/src/Toolkit.ts +109 -0
- package/src/index.ts +3 -0
- package/src/internal/auth.ts +48 -0
- package/src/internal/httpTransport.ts +171 -0
- package/src/internal/protocol.ts +67 -0
- package/src/internal/protocols/2025-06-18.ts +110 -0
- package/src/internal/protocols/2025-11-25.ts +8 -0
- package/src/internal/protocols/2026-07-28.test.ts +131 -0
- package/src/internal/protocols/2026-07-28.ts +152 -0
- package/src/internal/rpc.test.ts +156 -0
- package/src/internal/rpc.ts +189 -0
- package/src/internal/schema.ts +223 -0
- package/src/internal/stdioTransport.ts +63 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Era-blind JSON-RPC 2.0 correlation core over a `Transport` (cdp.ts model):
|
|
3
|
+
* pending-`Deferred` map keyed by id, reader fiber that fails every pending
|
|
4
|
+
* request on transport close. Era behavior layers on top in era.ts.
|
|
5
|
+
*/
|
|
6
|
+
import { Cause, Deferred, Effect, HashMap, Option, Ref, type Scope, Stream } from "effect"
|
|
7
|
+
import * as JSONL from "@effect-uai/core/JSONL"
|
|
8
|
+
import { type McpError, McpProtocolError, McpTransportClosed } from "../McpError.js"
|
|
9
|
+
import {
|
|
10
|
+
decodeInboundMessage,
|
|
11
|
+
type InboundMessage,
|
|
12
|
+
type JsonRpcId,
|
|
13
|
+
errorFrame,
|
|
14
|
+
notificationFrame,
|
|
15
|
+
requestFrame,
|
|
16
|
+
resultFrame,
|
|
17
|
+
} from "./schema.js"
|
|
18
|
+
|
|
19
|
+
/** Per-request transport hints (era headers on HTTP; stdio ignores them). */
|
|
20
|
+
export type SendMeta = {
|
|
21
|
+
readonly headers?: Record<string, string>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The transport seam: one framed JSON message per `send` / `messages` element. */
|
|
25
|
+
export type Transport = {
|
|
26
|
+
readonly send: (frame: string, meta?: SendMeta) => Effect.Effect<void, McpError>
|
|
27
|
+
readonly messages: Stream.Stream<string, McpError>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A server-initiated request (has `id`) or notification (no `id`). */
|
|
31
|
+
export type Inbound = {
|
|
32
|
+
readonly id: Option.Option<JsonRpcId>
|
|
33
|
+
readonly method: string
|
|
34
|
+
readonly params: unknown
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type McpConnection = {
|
|
38
|
+
readonly request: (
|
|
39
|
+
method: string,
|
|
40
|
+
params?: unknown,
|
|
41
|
+
meta?: SendMeta,
|
|
42
|
+
) => Effect.Effect<unknown, McpError>
|
|
43
|
+
readonly notify: (
|
|
44
|
+
method: string,
|
|
45
|
+
params?: unknown,
|
|
46
|
+
meta?: SendMeta,
|
|
47
|
+
) => Effect.Effect<void, McpError>
|
|
48
|
+
/** Answer a server-initiated request (legacy era only). */
|
|
49
|
+
readonly respond: (id: JsonRpcId, result: unknown) => Effect.Effect<void, McpError>
|
|
50
|
+
readonly respondError: (
|
|
51
|
+
id: JsonRpcId,
|
|
52
|
+
code: number,
|
|
53
|
+
message: string,
|
|
54
|
+
) => Effect.Effect<void, McpError>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type Pending = {
|
|
58
|
+
readonly method: string
|
|
59
|
+
readonly deferred: Deferred.Deferred<unknown, McpError>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const replyError = (method: string, error: NonNullable<InboundMessage["error"]>): McpError =>
|
|
63
|
+
new McpProtocolError({
|
|
64
|
+
method,
|
|
65
|
+
code: error.code,
|
|
66
|
+
...(error.message !== undefined ? { reason: error.message } : {}),
|
|
67
|
+
raw: error,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Open a scoped connection over an already-open transport. `onInbound`
|
|
72
|
+
* receives server-initiated frames; era.ts answers them via `transport.send`.
|
|
73
|
+
*/
|
|
74
|
+
export const open = (
|
|
75
|
+
transport: Transport,
|
|
76
|
+
onInbound: (inbound: Inbound) => Effect.Effect<void>,
|
|
77
|
+
): Effect.Effect<McpConnection, never, Scope.Scope> =>
|
|
78
|
+
Effect.gen(function* () {
|
|
79
|
+
const pending = yield* Ref.make(HashMap.empty<JsonRpcId, Pending>())
|
|
80
|
+
const counter = yield* Ref.make(0)
|
|
81
|
+
const closeCause = yield* Ref.make<unknown>(undefined)
|
|
82
|
+
|
|
83
|
+
// Fail every in-flight request with one error. Shared by transport close
|
|
84
|
+
// and by unattributable server errors: a caller must never be left
|
|
85
|
+
// awaiting a reply that cannot arrive.
|
|
86
|
+
const failAllPending = (toError: (method: string) => McpError): Effect.Effect<void> =>
|
|
87
|
+
Ref.getAndSet(pending, HashMap.empty()).pipe(
|
|
88
|
+
Effect.flatMap((taken) =>
|
|
89
|
+
Effect.forEach(HashMap.values(taken), ({ deferred, method }) =>
|
|
90
|
+
Deferred.fail(deferred, toError(method)),
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
Effect.asVoid,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// An error no pending request can claim: a null id (JSON-RPC raises one
|
|
97
|
+
// before the request could be attributed) or an id we never issued. It
|
|
98
|
+
// correlates to nothing, so everything in flight takes it rather than
|
|
99
|
+
// waiting for a reply that cannot arrive. A *result* nobody claims is a
|
|
100
|
+
// late or duplicate reply, and is simply dropped.
|
|
101
|
+
const unattributable = (error: InboundMessage["error"]): Effect.Effect<void> =>
|
|
102
|
+
error === undefined ? Effect.void : failAllPending((method) => replyError(method, error))
|
|
103
|
+
|
|
104
|
+
const settle = ({ deferred, method }: Pending, message: InboundMessage): Effect.Effect<void> =>
|
|
105
|
+
Effect.asVoid(
|
|
106
|
+
message.error === undefined
|
|
107
|
+
? Deferred.succeed(deferred, message.result ?? {})
|
|
108
|
+
: Deferred.fail(deferred, replyError(method, message.error)),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const reply = (id: JsonRpcId, message: InboundMessage): Effect.Effect<void> =>
|
|
112
|
+
Ref.modify(pending, (m) => [HashMap.get(m, id), HashMap.remove(m, id)]).pipe(
|
|
113
|
+
Effect.flatMap(
|
|
114
|
+
Option.match({
|
|
115
|
+
onNone: () => unattributable(message.error),
|
|
116
|
+
onSome: (entry) => settle(entry, message),
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
// A frame carrying a `method` is server-initiated; anything else is a
|
|
122
|
+
// reply, correlated by id.
|
|
123
|
+
const route = (message: InboundMessage): Effect.Effect<void> =>
|
|
124
|
+
Option.match(Option.fromNullishOr(message.method), {
|
|
125
|
+
onNone: () =>
|
|
126
|
+
Option.match(Option.fromNullishOr(message.id), {
|
|
127
|
+
onNone: () => unattributable(message.error),
|
|
128
|
+
onSome: (id) => reply(id, message),
|
|
129
|
+
}),
|
|
130
|
+
onSome: (method) =>
|
|
131
|
+
onInbound({ id: Option.fromNullishOr(message.id), method, params: message.params }),
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Unparseable and undecodable frames are dropped: one bad frame must not
|
|
135
|
+
// end an otherwise healthy connection.
|
|
136
|
+
const dispatch = (raw: string): Effect.Effect<void> =>
|
|
137
|
+
JSONL.parseSafe(raw).pipe(
|
|
138
|
+
Effect.flatMap((json) => Effect.option(decodeInboundMessage(json))),
|
|
139
|
+
Effect.flatMap(Option.match({ onNone: () => Effect.void, onSome: route })),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
// Fail every still-pending request on transport close so callers never hang.
|
|
143
|
+
const failPending = Effect.gen(function* () {
|
|
144
|
+
const raw = yield* Ref.get(closeCause)
|
|
145
|
+
yield* failAllPending(
|
|
146
|
+
(method) => new McpTransportClosed({ method, reason: "connection closed", raw }),
|
|
147
|
+
)
|
|
148
|
+
})
|
|
149
|
+
yield* transport.messages.pipe(
|
|
150
|
+
Stream.runForEach(dispatch),
|
|
151
|
+
Effect.tapCause((cause) => Ref.set(closeCause, Cause.squash(cause))),
|
|
152
|
+
Effect.ensuring(failPending),
|
|
153
|
+
Effect.forkScoped,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const request = (
|
|
157
|
+
method: string,
|
|
158
|
+
params?: unknown,
|
|
159
|
+
meta?: SendMeta,
|
|
160
|
+
): Effect.Effect<unknown, McpError> =>
|
|
161
|
+
Effect.gen(function* () {
|
|
162
|
+
const id: JsonRpcId = yield* Ref.updateAndGet(counter, (n) => n + 1)
|
|
163
|
+
const deferred = yield* Deferred.make<unknown, McpError>()
|
|
164
|
+
yield* Ref.update(pending, (m) => HashMap.set(m, id, { method, deferred }))
|
|
165
|
+
// `ensuring` reclaims the entry on send failure and interruption; on
|
|
166
|
+
// success dispatch has already removed it.
|
|
167
|
+
return yield* transport.send(requestFrame(id, method, params), meta).pipe(
|
|
168
|
+
Effect.flatMap(() => Deferred.await(deferred)),
|
|
169
|
+
Effect.ensuring(Ref.update(pending, (m) => HashMap.remove(m, id))),
|
|
170
|
+
)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const notify = (
|
|
174
|
+
method: string,
|
|
175
|
+
params?: unknown,
|
|
176
|
+
meta?: SendMeta,
|
|
177
|
+
): Effect.Effect<void, McpError> => transport.send(notificationFrame(method, params), meta)
|
|
178
|
+
|
|
179
|
+
const respond = (id: JsonRpcId, result: unknown): Effect.Effect<void, McpError> =>
|
|
180
|
+
transport.send(resultFrame(id, result))
|
|
181
|
+
|
|
182
|
+
const respondError = (
|
|
183
|
+
id: JsonRpcId,
|
|
184
|
+
code: number,
|
|
185
|
+
message: string,
|
|
186
|
+
): Effect.Effect<void, McpError> => transport.send(errorFrame(id, code, message))
|
|
187
|
+
|
|
188
|
+
return { request, notify, respond, respondError }
|
|
189
|
+
})
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-contained MCP wire schemas, both protocol eras. The single source of
|
|
3
|
+
* truth for the wire format; nothing MCP-shaped is imported from effect.
|
|
4
|
+
* Decodes are deliberately loose (unknown extra fields pass through) so
|
|
5
|
+
* spec-compliant servers with richer payloads never fail the client.
|
|
6
|
+
*/
|
|
7
|
+
import { Option, Schema } from "effect"
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Protocol versions
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/** Every protocol version this client can speak. */
|
|
14
|
+
export const ProtocolVersion = Schema.Literals(["2026-07-28", "2025-11-25", "2025-06-18"])
|
|
15
|
+
export type ProtocolVersion = typeof ProtocolVersion.Type
|
|
16
|
+
|
|
17
|
+
/** Newest version we speak: the stateless protocol, and what the probe tries first. */
|
|
18
|
+
export const LATEST_VERSION = "2026-07-28" satisfies ProtocolVersion
|
|
19
|
+
|
|
20
|
+
/** The legacy handshake era we negotiate; 2025-11-25 is wire-compatible. */
|
|
21
|
+
export const LEGACY_VERSION = "2025-06-18" satisfies ProtocolVersion
|
|
22
|
+
|
|
23
|
+
const isProtocolVersion = Schema.is(ProtocolVersion)
|
|
24
|
+
|
|
25
|
+
/** Narrow a server-offered version string to one we support. */
|
|
26
|
+
export const asProtocolVersion = (raw: string): Option.Option<ProtocolVersion> =>
|
|
27
|
+
isProtocolVersion(raw) ? Option.some(raw) : Option.none()
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The JSON-RPC methods this client issues. Not to be confused with an HTTP
|
|
31
|
+
* method: on Streamable HTTP every one of these travels as a POST, and the
|
|
32
|
+
* name is mirrored into the `Mcp-Method` header.
|
|
33
|
+
*/
|
|
34
|
+
export const McpMethod = Schema.Literals([
|
|
35
|
+
"server/discover",
|
|
36
|
+
"initialize",
|
|
37
|
+
"tools/list",
|
|
38
|
+
"tools/call",
|
|
39
|
+
])
|
|
40
|
+
export type McpMethod = typeof McpMethod.Type
|
|
41
|
+
|
|
42
|
+
/** Modern `_meta` keys (SEP-2575): every request self-describes its protocol. */
|
|
43
|
+
export const META_PROTOCOL_VERSION = "io.modelcontextprotocol/protocolVersion"
|
|
44
|
+
export const META_CLIENT_INFO = "io.modelcontextprotocol/clientInfo"
|
|
45
|
+
export const META_CLIENT_CAPABILITIES = "io.modelcontextprotocol/clientCapabilities"
|
|
46
|
+
/** Servers identify themselves in each *result's* `_meta`, not at the top level. */
|
|
47
|
+
export const META_SERVER_INFO = "io.modelcontextprotocol/serverInfo"
|
|
48
|
+
|
|
49
|
+
export const CLIENT_INFO = { name: "@effect-uai/mcp", version: "0.12.1" }
|
|
50
|
+
|
|
51
|
+
/** Modern JSON-RPC error codes. */
|
|
52
|
+
export const CODE_HEADER_MISMATCH = -32020
|
|
53
|
+
export const CODE_MISSING_CLIENT_CAPABILITY = -32021
|
|
54
|
+
export const CODE_UNSUPPORTED_PROTOCOL_VERSION = -32022
|
|
55
|
+
export const CODE_METHOD_NOT_FOUND = -32601
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A reply carrying one of these proves the server speaks a modern version, so
|
|
59
|
+
* era detection corrects and retries instead of falling back to `initialize`.
|
|
60
|
+
*/
|
|
61
|
+
export const MODERN_ERROR_CODES: ReadonlySet<number> = new Set([
|
|
62
|
+
CODE_HEADER_MISMATCH,
|
|
63
|
+
CODE_MISSING_CLIENT_CAPABILITY,
|
|
64
|
+
CODE_UNSUPPORTED_PROTOCOL_VERSION,
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// JSON-RPC envelope
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
export const JsonRpcId = Schema.Union([Schema.Number, Schema.String])
|
|
72
|
+
export type JsonRpcId = typeof JsonRpcId.Type
|
|
73
|
+
|
|
74
|
+
export const JsonRpcErrorObject = Schema.Struct({
|
|
75
|
+
code: Schema.Number,
|
|
76
|
+
message: Schema.optional(Schema.String),
|
|
77
|
+
data: Schema.optional(Schema.Unknown),
|
|
78
|
+
})
|
|
79
|
+
export type JsonRpcErrorObject = typeof JsonRpcErrorObject.Type
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Any inbound frame: a reply (`id` + `result`/`error`), a server-initiated
|
|
83
|
+
* request (`method` + `id`), or a notification (`method`, no `id`). One
|
|
84
|
+
* loose struct so the reader can classify without a union decode.
|
|
85
|
+
*/
|
|
86
|
+
export const InboundMessage = Schema.Struct({
|
|
87
|
+
// Null is legal and meaningful: JSON-RPC uses it for errors raised before a
|
|
88
|
+
// request could be attributed. Rejecting it would drop the frame entirely.
|
|
89
|
+
id: Schema.optional(Schema.NullOr(JsonRpcId)),
|
|
90
|
+
result: Schema.optional(Schema.Unknown),
|
|
91
|
+
error: Schema.optional(JsonRpcErrorObject),
|
|
92
|
+
method: Schema.optional(Schema.String),
|
|
93
|
+
params: Schema.optional(Schema.Unknown),
|
|
94
|
+
})
|
|
95
|
+
export type InboundMessage = typeof InboundMessage.Type
|
|
96
|
+
|
|
97
|
+
export const decodeInboundMessage = Schema.decodeUnknownEffect(InboundMessage)
|
|
98
|
+
|
|
99
|
+
// --- outbound frame builders -----------------------------------------------
|
|
100
|
+
|
|
101
|
+
export const requestFrame = (id: JsonRpcId, method: string, params: unknown): string =>
|
|
102
|
+
JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} })
|
|
103
|
+
|
|
104
|
+
export const notificationFrame = (method: string, params?: unknown): string =>
|
|
105
|
+
JSON.stringify(
|
|
106
|
+
params === undefined ? { jsonrpc: "2.0", method } : { jsonrpc: "2.0", method, params },
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
export const resultFrame = (id: JsonRpcId, result: unknown): string =>
|
|
110
|
+
JSON.stringify({ jsonrpc: "2.0", id, result })
|
|
111
|
+
|
|
112
|
+
export const errorFrame = (id: JsonRpcId, code: number, message: string): string =>
|
|
113
|
+
JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } })
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Shared: tools
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
export const ToolInfo = Schema.Struct({
|
|
120
|
+
name: Schema.String,
|
|
121
|
+
description: Schema.optional(Schema.String),
|
|
122
|
+
inputSchema: Schema.Record(Schema.String, Schema.Unknown),
|
|
123
|
+
})
|
|
124
|
+
export type ToolInfo = typeof ToolInfo.Type
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Modern list results may carry cache metadata (`ttlMs` / `cacheScope`);
|
|
128
|
+
* v1 reads but does not act on them (input for a later refresh feature).
|
|
129
|
+
*/
|
|
130
|
+
export const ListToolsResult = Schema.Struct({
|
|
131
|
+
tools: Schema.Array(ToolInfo),
|
|
132
|
+
nextCursor: Schema.optional(Schema.String),
|
|
133
|
+
ttlMs: Schema.optional(Schema.Number),
|
|
134
|
+
cacheScope: Schema.optional(Schema.String),
|
|
135
|
+
})
|
|
136
|
+
export type ListToolsResult = typeof ListToolsResult.Type
|
|
137
|
+
|
|
138
|
+
export const decodeListToolsResult = Schema.decodeUnknownEffect(ListToolsResult)
|
|
139
|
+
|
|
140
|
+
// --- content blocks ---------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
export const TextContent = Schema.Struct({
|
|
143
|
+
type: Schema.Literal("text"),
|
|
144
|
+
text: Schema.String,
|
|
145
|
+
})
|
|
146
|
+
export type TextContent = typeof TextContent.Type
|
|
147
|
+
|
|
148
|
+
/** Non-text blocks (image / audio / resource) are placeholder-summarized in v1. */
|
|
149
|
+
export const OtherContent = Schema.Struct({
|
|
150
|
+
type: Schema.String,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
export const ContentBlock = Schema.Union([TextContent, OtherContent])
|
|
154
|
+
export type ContentBlock = typeof ContentBlock.Type
|
|
155
|
+
|
|
156
|
+
export const isTextContent = Schema.is(TextContent)
|
|
157
|
+
|
|
158
|
+
export const CallToolResult = Schema.Struct({
|
|
159
|
+
content: Schema.optionalKey(Schema.Array(ContentBlock)),
|
|
160
|
+
isError: Schema.optional(Schema.Boolean),
|
|
161
|
+
structuredContent: Schema.optional(Schema.Unknown),
|
|
162
|
+
// Required on modern results; absent on legacy ones.
|
|
163
|
+
resultType: Schema.optional(Schema.String),
|
|
164
|
+
})
|
|
165
|
+
export type CallToolResult = typeof CallToolResult.Type
|
|
166
|
+
|
|
167
|
+
export const decodeCallToolResult = Schema.decodeUnknownEffect(CallToolResult)
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Modern era (2026-07-28)
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
export const ServerInfo = Schema.Struct({
|
|
174
|
+
name: Schema.String,
|
|
175
|
+
version: Schema.optional(Schema.String),
|
|
176
|
+
title: Schema.optional(Schema.String),
|
|
177
|
+
})
|
|
178
|
+
export type ServerInfo = typeof ServerInfo.Type
|
|
179
|
+
|
|
180
|
+
/** The subset of a result's `_meta` we read. */
|
|
181
|
+
export const ResultMeta = Schema.Struct({
|
|
182
|
+
[META_SERVER_INFO]: Schema.optional(ServerInfo),
|
|
183
|
+
})
|
|
184
|
+
export type ResultMeta = typeof ResultMeta.Type
|
|
185
|
+
|
|
186
|
+
export const DiscoverResult = Schema.Struct({
|
|
187
|
+
supportedVersions: Schema.Array(Schema.String),
|
|
188
|
+
capabilities: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
189
|
+
// Spec-conformant servers put identity in `_meta`; a top-level `serverInfo`
|
|
190
|
+
// is accepted too, since that is where the legacy handshake carries it.
|
|
191
|
+
_meta: Schema.optional(ResultMeta),
|
|
192
|
+
serverInfo: Schema.optional(ServerInfo),
|
|
193
|
+
instructions: Schema.optional(Schema.String),
|
|
194
|
+
})
|
|
195
|
+
export type DiscoverResult = typeof DiscoverResult.Type
|
|
196
|
+
|
|
197
|
+
export const decodeDiscoverResult = Schema.decodeUnknownEffect(DiscoverResult)
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* `data` of a `-32022` UnsupportedProtocolVersionError reply. Decoded, not
|
|
201
|
+
* hand-parsed: the shape is server-controlled and may carry extra fields.
|
|
202
|
+
*/
|
|
203
|
+
export const UnsupportedVersionData = Schema.Struct({
|
|
204
|
+
supported: Schema.optional(Schema.Array(Schema.String)),
|
|
205
|
+
requested: Schema.optional(Schema.NullOr(Schema.String)),
|
|
206
|
+
})
|
|
207
|
+
export type UnsupportedVersionData = typeof UnsupportedVersionData.Type
|
|
208
|
+
|
|
209
|
+
export const decodeUnsupportedVersionData = Schema.decodeUnknownOption(UnsupportedVersionData)
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Legacy era (2025-06-18 / 2025-11-25)
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
export const InitializeResult = Schema.Struct({
|
|
216
|
+
protocolVersion: Schema.String,
|
|
217
|
+
capabilities: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
218
|
+
serverInfo: Schema.optional(ServerInfo),
|
|
219
|
+
instructions: Schema.optional(Schema.String),
|
|
220
|
+
})
|
|
221
|
+
export type InitializeResult = typeof InitializeResult.Type
|
|
222
|
+
|
|
223
|
+
export const decodeInitializeResult = Schema.decodeUnknownEffect(InitializeResult)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stdio `Transport`: spawn the server, frame stdout as JSONL (one JSON message
|
|
3
|
+
* per line). Scoped, so the child is killed on scope close. Era-blind; the
|
|
4
|
+
* `meta.headers` a caller passes are HTTP-only and ignored here.
|
|
5
|
+
*/
|
|
6
|
+
import { Cause, Effect, Queue, type Scope, Stream } from "effect"
|
|
7
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
|
8
|
+
import * as JSONL from "@effect-uai/core/JSONL"
|
|
9
|
+
import { McpConnectFailed, type McpError, McpTransportClosed } from "../McpError.js"
|
|
10
|
+
import type { Transport } from "./rpc.js"
|
|
11
|
+
|
|
12
|
+
export type StdioConfig = {
|
|
13
|
+
readonly command: string
|
|
14
|
+
readonly args?: ReadonlyArray<string>
|
|
15
|
+
readonly env?: Record<string, string>
|
|
16
|
+
readonly cwd?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const encoder = new TextEncoder()
|
|
20
|
+
|
|
21
|
+
export const make = (
|
|
22
|
+
config: StdioConfig,
|
|
23
|
+
): Effect.Effect<Transport, McpError, Scope.Scope | ChildProcessSpawner.ChildProcessSpawner> =>
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
// Outbound frames go through a queue rendered as the child's stdin stream;
|
|
26
|
+
// `Queue.end` on scope close lets the child see a clean EOF.
|
|
27
|
+
const outbound = yield* Queue.make<Uint8Array, Cause.Done>()
|
|
28
|
+
const stdin = Stream.fromQueue(outbound)
|
|
29
|
+
|
|
30
|
+
const handle = yield* ChildProcess.make(config.command, [...(config.args ?? [])], {
|
|
31
|
+
...(config.cwd !== undefined ? { cwd: config.cwd } : {}),
|
|
32
|
+
...(config.env !== undefined ? { env: config.env } : {}),
|
|
33
|
+
stdin: { stream: stdin, endOnDone: true },
|
|
34
|
+
stdout: "pipe",
|
|
35
|
+
stderr: "pipe",
|
|
36
|
+
killSignal: "SIGTERM",
|
|
37
|
+
forceKillAfter: "2 seconds",
|
|
38
|
+
}).pipe(
|
|
39
|
+
Effect.mapError(
|
|
40
|
+
(cause) =>
|
|
41
|
+
new McpConnectFailed({ reason: `could not spawn ${config.command}`, raw: cause }),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
yield* Effect.addFinalizer(() => Queue.end(outbound))
|
|
45
|
+
|
|
46
|
+
const send = (frame: string): Effect.Effect<void, McpError> =>
|
|
47
|
+
Queue.offer(outbound, encoder.encode(`${frame}\n`)).pipe(
|
|
48
|
+
Effect.mapError(
|
|
49
|
+
(cause) =>
|
|
50
|
+
new McpTransportClosed({ reason: "server stdin closed", raw: cause }) as McpError,
|
|
51
|
+
),
|
|
52
|
+
Effect.asVoid,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const messages = handle.stdout.pipe(
|
|
56
|
+
JSONL.fromBytes,
|
|
57
|
+
Stream.mapError(
|
|
58
|
+
(cause) => new McpTransportClosed({ reason: "server stdout closed", raw: cause }),
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return { send, messages } satisfies Transport
|
|
63
|
+
})
|