@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/mcp-client",
3
- "version": "0.8.4-main.fcfe5033a5",
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": "MIT",
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.33.2",
26
+ "@effect/ai": "0.36.0",
27
27
  "@modelcontextprotocol/sdk": "^1.27.1",
28
- "effect": "3.20.0",
29
- "@dxos/ai": "0.8.4-main.fcfe5033a5",
30
- "@dxos/effect": "0.8.4-main.fcfe5033a5",
31
- "@dxos/log": "0.8.4-main.fcfe5033a5",
32
- "@dxos/invariant": "0.8.4-main.fcfe5033a5"
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"
@@ -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 { runAndForwardErrors } from '@dxos/effect';
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('@anthropic/claude-opus-4-6', { thinking: false }).pipe(
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
- kind: 'sse',
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
- kind: 'sse',
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
- kind: 'sse',
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
- export interface McpToolkitOptions {
19
- url: string;
20
- kind: 'sse' | 'http';
21
- apiKey?: string;
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 kind ('sse' for SSE, 'http' for Streamable HTTP).
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 const make = (options: McpToolkitOptions): Effect.Effect<OpaqueToolkit.OpaqueToolkit> =>
33
- Effect.gen(function* () {
34
- const client = yield* connectWithFallback(options);
44
+ export interface McpToolkitOptions {
45
+ url: string;
46
+ protocol: 'sse' | 'http';
47
+ apiKey?: string;
48
+ }
35
49
 
36
- const { tools } = yield* Effect.promise(() => client.listTools());
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 = (options: McpToolkitOptions): Effect.Effect<Client> =>
127
+ const connectWithFallback = (
128
+ options: McpToolkitOptions,
129
+ ): Effect.Effect<{ client: Client; protocol: McpToolkitOptions['protocol'] }, McpConnectionError> =>
99
130
  Effect.gen(function* () {
100
- const fallbackKind = options.kind === 'sse' ? 'http' : 'sse';
101
- const primary = yield* connectClient(options.url, options.kind, options.apiKey).pipe(Effect.either);
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
- return yield* connectClient(options.url, fallbackKind, options.apiKey).pipe(Effect.orDie);
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.die(primary.left);
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, kind: McpToolkitOptions['kind'], apiKey?: 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, kind, apiKey);
161
+ const transport = createTransport(url, protocol, apiKey);
115
162
  return client.connect(transport).then(() => client);
116
163
  });
117
164
 
118
165
  /**
119
- * Creates a transport for the given MCP server URL and kind.
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
- kind: McpToolkitOptions['kind'],
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 (kind) {
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 = kind;
135
- return invariant(false, `Unsupported MCP transport kind: ${_exhaustive}`) as never;
197
+ const _exhaustive: never = protocol;
198
+ return invariant(false, `Unsupported MCP transport protocol: ${_exhaustive}`) as never;
136
199
  }
137
200
  }
138
201
  };