@dxos/mcp-client 0.8.4-main.fcfe5033a5 → 0.9.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/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/neutral/index.mjs +57 -22
- package/dist/lib/neutral/index.mjs.map +3 -3
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/types/src/McpToolkit.d.ts +17 -2
- package/dist/types/src/McpToolkit.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/McpToolkit.test.ts +8 -8
- package/src/McpToolkit.ts +86 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/mcp-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "MCP client.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/dxos/dxos"
|
|
10
10
|
},
|
|
11
|
-
"license": "
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
12
12
|
"author": "info@dxos.org",
|
|
13
13
|
"type": "module",
|
|
14
14
|
"exports": {
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
"src"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@effect/ai": "0.
|
|
26
|
+
"@effect/ai": "0.36.0",
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
28
|
-
"effect": "3.
|
|
29
|
-
"@dxos/
|
|
30
|
-
"@dxos/
|
|
31
|
-
"@dxos/
|
|
32
|
-
"@dxos/
|
|
28
|
+
"effect": "3.21.3",
|
|
29
|
+
"@dxos/effect": "0.9.0",
|
|
30
|
+
"@dxos/invariant": "0.9.0",
|
|
31
|
+
"@dxos/ai": "0.9.0",
|
|
32
|
+
"@dxos/log": "0.9.0"
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
|
35
35
|
"access": "public"
|
package/src/McpToolkit.test.ts
CHANGED
|
@@ -12,13 +12,13 @@ import * as Schema from 'effect/Schema';
|
|
|
12
12
|
|
|
13
13
|
import { AiService, type OpaqueToolkit } from '@dxos/ai';
|
|
14
14
|
import { TestAiService } from '@dxos/ai/testing';
|
|
15
|
-
import {
|
|
15
|
+
import { EffectEx } from '@dxos/effect';
|
|
16
16
|
import { TestHelpers } from '@dxos/effect/testing';
|
|
17
17
|
import { log } from '@dxos/log';
|
|
18
18
|
|
|
19
19
|
import * as McpToolkit from './McpToolkit';
|
|
20
20
|
|
|
21
|
-
const AiServiceLayer = AiService.model('
|
|
21
|
+
const AiServiceLayer = AiService.model('ai.claude.model.claude-opus-4-6', { thinking: false }).pipe(
|
|
22
22
|
Layer.provide(
|
|
23
23
|
TestAiService({
|
|
24
24
|
disableMemoization: true,
|
|
@@ -34,10 +34,10 @@ describe('connectWithFallback', () => {
|
|
|
34
34
|
timeout: 30_000,
|
|
35
35
|
},
|
|
36
36
|
async () => {
|
|
37
|
-
const toolkit = await runAndForwardErrors(
|
|
37
|
+
const toolkit = await EffectEx.runAndForwardErrors(
|
|
38
38
|
McpToolkit.make({
|
|
39
39
|
url: 'https://mcp.linear.app/mcp',
|
|
40
|
-
|
|
40
|
+
protocol: 'sse',
|
|
41
41
|
apiKey: process.env.LINEAR_API_KEY,
|
|
42
42
|
}),
|
|
43
43
|
);
|
|
@@ -53,7 +53,7 @@ describe('Browser Automation', () => {
|
|
|
53
53
|
function* (_) {
|
|
54
54
|
const browserAutomationToolkit = yield* McpToolkit.make({
|
|
55
55
|
url: 'https://playwright-mcp-example.dxos.workers.dev/sse',
|
|
56
|
-
|
|
56
|
+
protocol: 'sse',
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
const chat = yield* Chat.empty;
|
|
@@ -78,10 +78,10 @@ describe('Browser Automation', () => {
|
|
|
78
78
|
},
|
|
79
79
|
Effect.provide(AiServiceLayer),
|
|
80
80
|
TestHelpers.provideTestContext,
|
|
81
|
-
TestHelpers.taggedTest('llm'),
|
|
82
81
|
),
|
|
83
82
|
{
|
|
84
83
|
timeout: 120_000,
|
|
84
|
+
tags: ['llm'],
|
|
85
85
|
},
|
|
86
86
|
);
|
|
87
87
|
|
|
@@ -91,7 +91,7 @@ describe('Browser Automation', () => {
|
|
|
91
91
|
function* (_) {
|
|
92
92
|
const browserAutomationToolkit = yield* McpToolkit.make({
|
|
93
93
|
url: 'https://playwright-mcp-example.dxos.workers.dev/sse',
|
|
94
|
-
|
|
94
|
+
protocol: 'sse',
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
const chat = yield* Chat.empty;
|
|
@@ -139,10 +139,10 @@ describe('Browser Automation', () => {
|
|
|
139
139
|
},
|
|
140
140
|
Effect.provide(AiServiceLayer),
|
|
141
141
|
TestHelpers.provideTestContext,
|
|
142
|
-
TestHelpers.taggedTest('llm'),
|
|
143
142
|
),
|
|
144
143
|
{
|
|
145
144
|
timeout: 120_000,
|
|
145
|
+
tags: ['llm'],
|
|
146
146
|
},
|
|
147
147
|
);
|
|
148
148
|
});
|
package/src/McpToolkit.ts
CHANGED
|
@@ -7,33 +7,59 @@
|
|
|
7
7
|
import * as Tool from '@effect/ai/Tool';
|
|
8
8
|
import * as Toolkit from '@effect/ai/Toolkit';
|
|
9
9
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
10
|
+
// SSEClientTransport is marked @deprecated in the SDK in favor of StreamableHTTP, but the
|
|
11
|
+
// SDK itself notes that clients should keep supporting both while servers migrate.
|
|
12
|
+
// `connectWithFallback` below tries the configured protocol first, then the other on 405.
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
10
14
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
11
15
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
16
|
+
import * as Cause from 'effect/Cause';
|
|
12
17
|
import * as Effect from 'effect/Effect';
|
|
13
18
|
import * as Schema from 'effect/Schema';
|
|
14
19
|
|
|
15
20
|
import { OpaqueToolkit } from '@dxos/ai';
|
|
16
21
|
import { invariant } from '@dxos/invariant';
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Typed failure raised when the MCP client cannot connect or list tools.
|
|
25
|
+
* Carries the underlying cause and the server URL for diagnostics.
|
|
26
|
+
*/
|
|
27
|
+
export class McpConnectionError extends Schema.TaggedError<McpConnectionError>('McpConnectionError')(
|
|
28
|
+
'McpConnectionError',
|
|
29
|
+
{
|
|
30
|
+
url: Schema.String,
|
|
31
|
+
protocol: Schema.Literal('sse', 'http'),
|
|
32
|
+
message: Schema.String,
|
|
33
|
+
},
|
|
34
|
+
) {}
|
|
23
35
|
|
|
24
36
|
/**
|
|
25
37
|
* Creates an OpaqueToolkit that connects to an MCP server and exposes its tools to the assistant.
|
|
26
38
|
*
|
|
27
|
-
* @param options MCP server URL and transport
|
|
39
|
+
* @param options MCP server URL and transport protocol ('http' for Streamable HTTP, 'sse' for SSE).
|
|
28
40
|
* @returns An OpaqueToolkit containing all tools from the MCP server.
|
|
29
41
|
*/
|
|
30
42
|
const CLIENT_INFO = { name: '@dxos/mcp-client', version: '0.8.3' };
|
|
31
43
|
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
export interface McpToolkitOptions {
|
|
45
|
+
url: string;
|
|
46
|
+
protocol: 'sse' | 'http';
|
|
47
|
+
apiKey?: string;
|
|
48
|
+
}
|
|
35
49
|
|
|
36
|
-
|
|
50
|
+
export const make = (options: McpToolkitOptions): Effect.Effect<OpaqueToolkit.OpaqueToolkit, McpConnectionError> =>
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
const { client, protocol } = yield* connectWithFallback(options);
|
|
53
|
+
|
|
54
|
+
const { tools } = yield* Effect.tryPromise({
|
|
55
|
+
try: () => client.listTools(),
|
|
56
|
+
catch: (cause) =>
|
|
57
|
+
new McpConnectionError({
|
|
58
|
+
url: options.url,
|
|
59
|
+
protocol,
|
|
60
|
+
message: `Failed to list MCP tools: ${formatCause(cause)}`,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
37
63
|
if (tools.length === 0) {
|
|
38
64
|
return OpaqueToolkit.empty;
|
|
39
65
|
}
|
|
@@ -94,45 +120,82 @@ export const is405 = (error: unknown): boolean => {
|
|
|
94
120
|
* Connects to an MCP server, falling back to the alternate transport on 405 errors.
|
|
95
121
|
* Per the MCP spec, a 405 indicates the server uses the other transport protocol.
|
|
96
122
|
* Returns the connected Client (a fresh instance is created for the fallback attempt).
|
|
123
|
+
*
|
|
124
|
+
* Failures are surfaced as typed `McpConnectionError` so callers can recover (e.g. drop
|
|
125
|
+
* the misconfigured server) without breaking the surrounding effect.
|
|
97
126
|
*/
|
|
98
|
-
const connectWithFallback = (
|
|
127
|
+
const connectWithFallback = (
|
|
128
|
+
options: McpToolkitOptions,
|
|
129
|
+
): Effect.Effect<{ client: Client; protocol: McpToolkitOptions['protocol'] }, McpConnectionError> =>
|
|
99
130
|
Effect.gen(function* () {
|
|
100
|
-
const
|
|
101
|
-
const primary = yield* connectClient(options.url, options.
|
|
131
|
+
const fallbackProtocol = options.protocol === 'sse' ? 'http' : 'sse';
|
|
132
|
+
const primary = yield* connectClient(options.url, options.protocol, options.apiKey).pipe(Effect.either);
|
|
102
133
|
if (primary._tag === 'Right') {
|
|
103
|
-
return primary.right;
|
|
134
|
+
return { client: primary.right, protocol: options.protocol };
|
|
104
135
|
}
|
|
105
136
|
if (is405(primary.left)) {
|
|
106
|
-
|
|
137
|
+
const fallback = yield* connectClient(options.url, fallbackProtocol, options.apiKey).pipe(Effect.either);
|
|
138
|
+
if (fallback._tag === 'Right') {
|
|
139
|
+
return { client: fallback.right, protocol: fallbackProtocol };
|
|
140
|
+
}
|
|
141
|
+
return yield* Effect.fail(
|
|
142
|
+
new McpConnectionError({
|
|
143
|
+
url: options.url,
|
|
144
|
+
protocol: fallbackProtocol,
|
|
145
|
+
message: `Failed to connect via ${fallbackProtocol} after 405 fallback: ${formatCause(fallback.left)}`,
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
107
148
|
}
|
|
108
|
-
return yield* Effect.
|
|
149
|
+
return yield* Effect.fail(
|
|
150
|
+
new McpConnectionError({
|
|
151
|
+
url: options.url,
|
|
152
|
+
protocol: options.protocol,
|
|
153
|
+
message: `Failed to connect via ${options.protocol}: ${formatCause(primary.left)}`,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
109
156
|
});
|
|
110
157
|
|
|
111
|
-
const connectClient = (url: string,
|
|
158
|
+
const connectClient = (url: string, protocol: McpToolkitOptions['protocol'], apiKey?: string) =>
|
|
112
159
|
Effect.tryPromise(() => {
|
|
113
160
|
const client = new Client(CLIENT_INFO);
|
|
114
|
-
const transport = createTransport(url,
|
|
161
|
+
const transport = createTransport(url, protocol, apiKey);
|
|
115
162
|
return client.connect(transport).then(() => client);
|
|
116
163
|
});
|
|
117
164
|
|
|
118
165
|
/**
|
|
119
|
-
*
|
|
166
|
+
* Renders a thrown value to a short string for inclusion in error messages.
|
|
167
|
+
* `Effect.tryPromise` wraps thrown errors in `UnknownException`; unwrap when present.
|
|
168
|
+
*/
|
|
169
|
+
const formatCause = (error: unknown): string => {
|
|
170
|
+
const inner = error != null && typeof error === 'object' && 'error' in error ? (error as any).error : error;
|
|
171
|
+
if (inner instanceof Error) {
|
|
172
|
+
return inner.message;
|
|
173
|
+
}
|
|
174
|
+
if (Cause.isCause(error)) {
|
|
175
|
+
return Cause.pretty(error);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return String(inner);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Creates a transport for the given MCP server URL and protocol.
|
|
120
183
|
*/
|
|
121
184
|
const createTransport = (
|
|
122
185
|
url: string,
|
|
123
|
-
|
|
186
|
+
protocol: McpToolkitOptions['protocol'],
|
|
124
187
|
apiKey?: string,
|
|
125
188
|
): SSEClientTransport | StreamableHTTPClientTransport => {
|
|
126
189
|
const urlObj = new URL(url);
|
|
127
190
|
const requestInit: RequestInit | undefined = apiKey ? { headers: { Authorization: `Bearer ${apiKey}` } } : undefined;
|
|
128
|
-
switch (
|
|
191
|
+
switch (protocol) {
|
|
129
192
|
case 'sse':
|
|
130
193
|
return new SSEClientTransport(urlObj, { requestInit });
|
|
131
194
|
case 'http':
|
|
132
195
|
return new StreamableHTTPClientTransport(urlObj, { requestInit });
|
|
133
196
|
default: {
|
|
134
|
-
const _exhaustive: never =
|
|
135
|
-
return invariant(false, `Unsupported MCP transport
|
|
197
|
+
const _exhaustive: never = protocol;
|
|
198
|
+
return invariant(false, `Unsupported MCP transport protocol: ${_exhaustive}`) as never;
|
|
136
199
|
}
|
|
137
200
|
}
|
|
138
201
|
};
|