@strands-agents/sdk 1.0.0 → 1.2.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/README.md +6 -0
- package/dist/src/__fixtures__/agent-helpers.d.ts +16 -1
- package/dist/src/__fixtures__/agent-helpers.d.ts.map +1 -1
- package/dist/src/__fixtures__/agent-helpers.js +42 -0
- package/dist/src/__fixtures__/agent-helpers.js.map +1 -1
- package/dist/src/__fixtures__/tool-helpers.d.ts +2 -1
- package/dist/src/__fixtures__/tool-helpers.d.ts.map +1 -1
- package/dist/src/__fixtures__/tool-helpers.js +20 -3
- package/dist/src/__fixtures__/tool-helpers.js.map +1 -1
- package/dist/src/__tests__/interrupt.test.d.ts +2 -0
- package/dist/src/__tests__/interrupt.test.d.ts.map +1 -0
- package/dist/src/__tests__/interrupt.test.js +264 -0
- package/dist/src/__tests__/interrupt.test.js.map +1 -0
- package/dist/src/__tests__/mcp.test.js +447 -7
- package/dist/src/__tests__/mcp.test.js.map +1 -1
- package/dist/src/agent/__tests__/agent.hook.test.js +551 -1
- package/dist/src/agent/__tests__/agent.hook.test.js.map +1 -1
- package/dist/src/agent/__tests__/agent.interrupt.test.d.ts +2 -0
- package/dist/src/agent/__tests__/agent.interrupt.test.d.ts.map +1 -0
- package/dist/src/agent/__tests__/agent.interrupt.test.js +779 -0
- package/dist/src/agent/__tests__/agent.interrupt.test.js.map +1 -0
- package/dist/src/agent/__tests__/agent.model-retry.test.d.ts +2 -0
- package/dist/src/agent/__tests__/agent.model-retry.test.d.ts.map +1 -0
- package/dist/src/agent/__tests__/agent.model-retry.test.js +161 -0
- package/dist/src/agent/__tests__/agent.model-retry.test.js.map +1 -0
- package/dist/src/agent/__tests__/agent.test.js +174 -0
- package/dist/src/agent/__tests__/agent.test.js.map +1 -1
- package/dist/src/agent/__tests__/snapshot.test.js +148 -4
- package/dist/src/agent/__tests__/snapshot.test.js.map +1 -1
- package/dist/src/agent/agent-as-tool.d.ts.map +1 -1
- package/dist/src/agent/agent-as-tool.js +2 -3
- package/dist/src/agent/agent-as-tool.js.map +1 -1
- package/dist/src/agent/agent.d.ts +94 -4
- package/dist/src/agent/agent.d.ts.map +1 -1
- package/dist/src/agent/agent.js +625 -223
- package/dist/src/agent/agent.js.map +1 -1
- package/dist/src/agent/snapshot.d.ts +11 -19
- package/dist/src/agent/snapshot.d.ts.map +1 -1
- package/dist/src/agent/snapshot.js +23 -19
- package/dist/src/agent/snapshot.js.map +1 -1
- package/dist/src/conversation-manager/__tests__/conversation-manager.test.js +230 -9
- package/dist/src/conversation-manager/__tests__/conversation-manager.test.js.map +1 -1
- package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js +19 -6
- package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js.map +1 -1
- package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js +422 -41
- package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js.map +1 -1
- package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js +75 -1
- package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js.map +1 -1
- package/dist/src/conversation-manager/conversation-manager.d.ts +67 -22
- package/dist/src/conversation-manager/conversation-manager.d.ts.map +1 -1
- package/dist/src/conversation-manager/conversation-manager.js +65 -13
- package/dist/src/conversation-manager/conversation-manager.js.map +1 -1
- package/dist/src/conversation-manager/index.d.ts +1 -1
- package/dist/src/conversation-manager/index.d.ts.map +1 -1
- package/dist/src/conversation-manager/index.js +1 -1
- package/dist/src/conversation-manager/index.js.map +1 -1
- package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts +43 -10
- package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts.map +1 -1
- package/dist/src/conversation-manager/sliding-window-conversation-manager.js +202 -45
- package/dist/src/conversation-manager/sliding-window-conversation-manager.js.map +1 -1
- package/dist/src/conversation-manager/summarizing-conversation-manager.d.ts +23 -1
- package/dist/src/conversation-manager/summarizing-conversation-manager.d.ts.map +1 -1
- package/dist/src/conversation-manager/summarizing-conversation-manager.js +39 -17
- package/dist/src/conversation-manager/summarizing-conversation-manager.js.map +1 -1
- package/dist/src/hooks/__tests__/events.test.js +99 -12
- package/dist/src/hooks/__tests__/events.test.js.map +1 -1
- package/dist/src/hooks/__tests__/registry.test.js +166 -2
- package/dist/src/hooks/__tests__/registry.test.js.map +1 -1
- package/dist/src/hooks/events.d.ts +125 -32
- package/dist/src/hooks/events.d.ts.map +1 -1
- package/dist/src/hooks/events.js +111 -8
- package/dist/src/hooks/events.js.map +1 -1
- package/dist/src/hooks/index.d.ts +4 -3
- package/dist/src/hooks/index.d.ts.map +1 -1
- package/dist/src/hooks/index.js +2 -1
- package/dist/src/hooks/index.js.map +1 -1
- package/dist/src/hooks/registry.d.ts +12 -12
- package/dist/src/hooks/registry.d.ts.map +1 -1
- package/dist/src/hooks/registry.js +55 -15
- package/dist/src/hooks/registry.js.map +1 -1
- package/dist/src/hooks/types.d.ts +23 -0
- package/dist/src/hooks/types.d.ts.map +1 -1
- package/dist/src/hooks/types.js +17 -1
- package/dist/src/hooks/types.js.map +1 -1
- package/dist/src/index.d.ts +12 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/interrupt.d.ts +247 -0
- package/dist/src/interrupt.d.ts.map +1 -0
- package/dist/src/interrupt.js +316 -0
- package/dist/src/interrupt.js.map +1 -0
- package/dist/src/mcp.d.ts +61 -4
- package/dist/src/mcp.d.ts.map +1 -1
- package/dist/src/mcp.js +161 -25
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/models/__tests__/anthropic.test.js +78 -8
- package/dist/src/models/__tests__/anthropic.test.js.map +1 -1
- package/dist/src/models/__tests__/bedrock.test.js +156 -18
- package/dist/src/models/__tests__/bedrock.test.js.map +1 -1
- package/dist/src/models/__tests__/defaults.test.d.ts +2 -0
- package/dist/src/models/__tests__/defaults.test.d.ts.map +1 -0
- package/dist/src/models/__tests__/defaults.test.js +36 -0
- package/dist/src/models/__tests__/defaults.test.js.map +1 -0
- package/dist/src/models/__tests__/google.test.js +72 -6
- package/dist/src/models/__tests__/google.test.js.map +1 -1
- package/dist/src/models/anthropic.d.ts +10 -0
- package/dist/src/models/anthropic.d.ts.map +1 -1
- package/dist/src/models/anthropic.js +14 -4
- package/dist/src/models/anthropic.js.map +1 -1
- package/dist/src/models/bedrock.d.ts +17 -3
- package/dist/src/models/bedrock.d.ts.map +1 -1
- package/dist/src/models/bedrock.js +80 -13
- package/dist/src/models/bedrock.js.map +1 -1
- package/dist/src/models/defaults.d.ts +10 -0
- package/dist/src/models/defaults.d.ts.map +1 -1
- package/dist/src/models/defaults.js +129 -0
- package/dist/src/models/defaults.js.map +1 -1
- package/dist/src/models/google/model.d.ts.map +1 -1
- package/dist/src/models/google/model.js +4 -2
- package/dist/src/models/google/model.js.map +1 -1
- package/dist/src/models/google/types.d.ts +10 -0
- package/dist/src/models/google/types.d.ts.map +1 -1
- package/dist/src/models/model.d.ts +15 -0
- package/dist/src/models/model.d.ts.map +1 -1
- package/dist/src/models/model.js +18 -0
- package/dist/src/models/model.js.map +1 -1
- package/dist/src/models/openai/__tests__/chat.test.js +55 -2
- package/dist/src/models/openai/__tests__/chat.test.js.map +1 -1
- package/dist/src/models/openai/__tests__/responses.test.js +19 -0
- package/dist/src/models/openai/__tests__/responses.test.js.map +1 -1
- package/dist/src/models/openai/errors.d.ts.map +1 -1
- package/dist/src/models/openai/errors.js +7 -4
- package/dist/src/models/openai/errors.js.map +1 -1
- package/dist/src/models/openai/model.d.ts.map +1 -1
- package/dist/src/models/openai/model.js +2 -2
- package/dist/src/models/openai/model.js.map +1 -1
- package/dist/src/multiagent/__tests__/graph.test.js +69 -0
- package/dist/src/multiagent/__tests__/graph.test.js.map +1 -1
- package/dist/src/multiagent/__tests__/graph.tracer.test.js +14 -0
- package/dist/src/multiagent/__tests__/graph.tracer.test.js.map +1 -1
- package/dist/src/multiagent/__tests__/interrupts.test.d.ts +2 -0
- package/dist/src/multiagent/__tests__/interrupts.test.d.ts.map +1 -0
- package/dist/src/multiagent/__tests__/interrupts.test.js +390 -0
- package/dist/src/multiagent/__tests__/interrupts.test.js.map +1 -0
- package/dist/src/multiagent/__tests__/nodes.test.js +13 -0
- package/dist/src/multiagent/__tests__/nodes.test.js.map +1 -1
- package/dist/src/multiagent/__tests__/state.test.js +139 -1
- package/dist/src/multiagent/__tests__/state.test.js.map +1 -1
- package/dist/src/multiagent/__tests__/swarm.test.js +77 -0
- package/dist/src/multiagent/__tests__/swarm.test.js.map +1 -1
- package/dist/src/multiagent/events.d.ts +15 -1
- package/dist/src/multiagent/events.d.ts.map +1 -1
- package/dist/src/multiagent/events.js +18 -0
- package/dist/src/multiagent/events.js.map +1 -1
- package/dist/src/multiagent/graph.d.ts +59 -3
- package/dist/src/multiagent/graph.d.ts.map +1 -1
- package/dist/src/multiagent/graph.js +201 -34
- package/dist/src/multiagent/graph.js.map +1 -1
- package/dist/src/multiagent/multiagent.d.ts +77 -3
- package/dist/src/multiagent/multiagent.d.ts.map +1 -1
- package/dist/src/multiagent/multiagent.js +115 -1
- package/dist/src/multiagent/multiagent.js.map +1 -1
- package/dist/src/multiagent/nodes.d.ts +18 -0
- package/dist/src/multiagent/nodes.d.ts.map +1 -1
- package/dist/src/multiagent/nodes.js +69 -22
- package/dist/src/multiagent/nodes.js.map +1 -1
- package/dist/src/multiagent/state.d.ts +39 -3
- package/dist/src/multiagent/state.d.ts.map +1 -1
- package/dist/src/multiagent/state.js +80 -1
- package/dist/src/multiagent/state.js.map +1 -1
- package/dist/src/multiagent/swarm.d.ts +30 -1
- package/dist/src/multiagent/swarm.d.ts.map +1 -1
- package/dist/src/multiagent/swarm.js +166 -33
- package/dist/src/multiagent/swarm.js.map +1 -1
- package/dist/src/registry/__tests__/tool-registry.test.js +37 -0
- package/dist/src/registry/__tests__/tool-registry.test.js.map +1 -1
- package/dist/src/registry/tool-registry.d.ts +13 -7
- package/dist/src/registry/tool-registry.d.ts.map +1 -1
- package/dist/src/registry/tool-registry.js +35 -10
- package/dist/src/registry/tool-registry.js.map +1 -1
- package/dist/src/retry/__tests__/backoff-strategy.test.d.ts +2 -0
- package/dist/src/retry/__tests__/backoff-strategy.test.d.ts.map +1 -0
- package/dist/src/retry/__tests__/backoff-strategy.test.js +116 -0
- package/dist/src/retry/__tests__/backoff-strategy.test.js.map +1 -0
- package/dist/src/retry/__tests__/default-model-retry-strategy.test.d.ts +2 -0
- package/dist/src/retry/__tests__/default-model-retry-strategy.test.d.ts.map +1 -0
- package/dist/src/retry/__tests__/default-model-retry-strategy.test.js +225 -0
- package/dist/src/retry/__tests__/default-model-retry-strategy.test.js.map +1 -0
- package/dist/src/retry/backoff-strategy.d.ts +108 -0
- package/dist/src/retry/backoff-strategy.d.ts.map +1 -0
- package/dist/src/retry/backoff-strategy.js +86 -0
- package/dist/src/retry/backoff-strategy.js.map +1 -0
- package/dist/src/retry/default-model-retry-strategy.d.ts +76 -0
- package/dist/src/retry/default-model-retry-strategy.d.ts.map +1 -0
- package/dist/src/retry/default-model-retry-strategy.js +104 -0
- package/dist/src/retry/default-model-retry-strategy.js.map +1 -0
- package/dist/src/retry/index.d.ts +8 -0
- package/dist/src/retry/index.d.ts.map +1 -0
- package/dist/src/retry/index.js +7 -0
- package/dist/src/retry/index.js.map +1 -0
- package/dist/src/retry/model-retry-strategy.d.ts +80 -0
- package/dist/src/retry/model-retry-strategy.d.ts.map +1 -0
- package/dist/src/retry/model-retry-strategy.js +85 -0
- package/dist/src/retry/model-retry-strategy.js.map +1 -0
- package/dist/src/retry/retry-strategy.d.ts +34 -0
- package/dist/src/retry/retry-strategy.d.ts.map +1 -0
- package/dist/src/retry/retry-strategy.js +25 -0
- package/dist/src/retry/retry-strategy.js.map +1 -0
- package/dist/src/session/__tests__/session-manager.test.js +84 -3
- package/dist/src/session/__tests__/session-manager.test.js.map +1 -1
- package/dist/src/session/session-manager.d.ts +11 -2
- package/dist/src/session/session-manager.d.ts.map +1 -1
- package/dist/src/session/session-manager.js +17 -6
- package/dist/src/session/session-manager.js.map +1 -1
- package/dist/src/telemetry/__tests__/meter.test.js +5 -27
- package/dist/src/telemetry/__tests__/meter.test.js.map +1 -1
- package/dist/src/telemetry/meter.d.ts +12 -4
- package/dist/src/telemetry/meter.d.ts.map +1 -1
- package/dist/src/telemetry/meter.js +13 -8
- package/dist/src/telemetry/meter.js.map +1 -1
- package/dist/src/tools/__tests__/tool.test.js +24 -1
- package/dist/src/tools/__tests__/tool.test.js.map +1 -1
- package/dist/src/tools/function-tool.d.ts.map +1 -1
- package/dist/src/tools/function-tool.js +6 -1
- package/dist/src/tools/function-tool.js.map +1 -1
- package/dist/src/tools/mcp-tool.d.ts.map +1 -1
- package/dist/src/tools/mcp-tool.js +3 -2
- package/dist/src/tools/mcp-tool.js.map +1 -1
- package/dist/src/tools/tool.d.ts +10 -1
- package/dist/src/tools/tool.d.ts.map +1 -1
- package/dist/src/tools/tool.js +12 -0
- package/dist/src/tools/tool.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/src/types/__tests__/agent.test.js +97 -0
- package/dist/src/types/__tests__/agent.test.js.map +1 -1
- package/dist/src/types/agent.d.ts +48 -8
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/agent.js +28 -3
- package/dist/src/types/agent.js.map +1 -1
- package/dist/src/types/interrupt.d.ts +103 -0
- package/dist/src/types/interrupt.d.ts.map +1 -0
- package/dist/src/types/interrupt.js +63 -0
- package/dist/src/types/interrupt.js.map +1 -0
- package/dist/src/types/messages.d.ts +2 -1
- package/dist/src/types/messages.d.ts.map +1 -1
- package/dist/src/types/messages.js.map +1 -1
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.d.ts +2 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js +292 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.d.ts +2 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.js +148 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.d.ts +2 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.js +78 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/index.d.ts +23 -0
- package/dist/src/vended-plugins/context-offloader/index.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/index.js +21 -0
- package/dist/src/vended-plugins/context-offloader/index.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/plugin.d.ts +48 -0
- package/dist/src/vended-plugins/context-offloader/plugin.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/plugin.js +244 -0
- package/dist/src/vended-plugins/context-offloader/plugin.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/storage.d.ts +114 -0
- package/dist/src/vended-plugins/context-offloader/storage.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/storage.js +204 -0
- package/dist/src/vended-plugins/context-offloader/storage.js.map +1 -0
- package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js +12 -0
- package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js.map +1 -1
- package/dist/src/vended-tools/bash/__tests__/bash.test.node.js +3 -0
- package/dist/src/vended-tools/bash/__tests__/bash.test.node.js.map +1 -1
- package/dist/src/vended-tools/bash/bash.d.ts.map +1 -1
- package/dist/src/vended-tools/bash/bash.js +0 -3
- package/dist/src/vended-tools/bash/bash.js.map +1 -1
- package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js +3 -0
- package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js.map +1 -1
- package/dist/src/vended-tools/notebook/__tests__/notebook.test.js +3 -0
- package/dist/src/vended-tools/notebook/__tests__/notebook.test.js.map +1 -1
- package/dist/src/vended-tools/notebook/notebook.d.ts +1 -1
- package/package.json +9 -5
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
3
3
|
import { McpError, ErrorCode, ElicitRequestSchema, UrlElicitationRequiredError, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
5
|
+
import { ClientCredentialsProvider } from '@modelcontextprotocol/sdk/client/auth-extensions.js';
|
|
4
6
|
import { McpClient } from '../mcp.js';
|
|
5
7
|
import { McpTool } from '../tools/mcp-tool.js';
|
|
6
8
|
import { JsonBlock } from '../types/messages.js';
|
|
7
9
|
import { ImageBlock } from '../types/media.js';
|
|
8
10
|
import { context, propagation, trace, TraceFlags } from '@opentelemetry/api';
|
|
11
|
+
import { logger } from '../logging/index.js';
|
|
9
12
|
/**
|
|
10
13
|
* Helper to create a mock async generator that yields a result message.
|
|
11
14
|
* This simulates the behavior of callToolStream returning a stream that ends with a result.
|
|
@@ -15,6 +18,16 @@ function createMockCallToolStream(result) {
|
|
|
15
18
|
yield { type: 'result', result };
|
|
16
19
|
};
|
|
17
20
|
}
|
|
21
|
+
vi.mock('@modelcontextprotocol/sdk/client/streamableHttp.js', () => ({
|
|
22
|
+
StreamableHTTPClientTransport: vi.fn(function () {
|
|
23
|
+
return { start: vi.fn(), send: vi.fn(), close: vi.fn() };
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
26
|
+
vi.mock('@modelcontextprotocol/sdk/client/auth-extensions.js', () => ({
|
|
27
|
+
ClientCredentialsProvider: vi.fn(function () {
|
|
28
|
+
return { redirectUrl: undefined, clientMetadata: { client_id: 'test' } };
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
18
31
|
vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
|
|
19
32
|
Client: vi.fn(function () {
|
|
20
33
|
return {
|
|
@@ -23,6 +36,10 @@ vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
|
|
|
23
36
|
listTools: vi.fn(),
|
|
24
37
|
callTool: vi.fn(),
|
|
25
38
|
setRequestHandler: vi.fn(),
|
|
39
|
+
setNotificationHandler: vi.fn(),
|
|
40
|
+
getServerCapabilities: vi.fn(),
|
|
41
|
+
getServerVersion: vi.fn(),
|
|
42
|
+
getInstructions: vi.fn(),
|
|
26
43
|
experimental: {
|
|
27
44
|
tasks: {
|
|
28
45
|
callToolStream: vi.fn(),
|
|
@@ -110,7 +127,11 @@ describe('MCP Integration', () => {
|
|
|
110
127
|
sdkClientMock = vi.mocked(Client).mock.results[0].value;
|
|
111
128
|
});
|
|
112
129
|
it('initializes SDK client with correct configuration', () => {
|
|
113
|
-
expect(Client).toHaveBeenCalledWith({ name: 'TestApp', version: '0.0.1' },
|
|
130
|
+
expect(Client).toHaveBeenCalledWith({ name: 'TestApp', version: '0.0.1' }, expect.objectContaining({
|
|
131
|
+
listChanged: expect.objectContaining({
|
|
132
|
+
tools: expect.objectContaining({ autoRefresh: false, debounceMs: 300 }),
|
|
133
|
+
}),
|
|
134
|
+
}));
|
|
114
135
|
});
|
|
115
136
|
it('injects trace context into tool arguments when active span exists', async () => {
|
|
116
137
|
mockActiveSpan();
|
|
@@ -198,17 +219,72 @@ describe('MCP Integration', () => {
|
|
|
198
219
|
expect(tools[0]).toBeInstanceOf(McpTool);
|
|
199
220
|
expect(tools[0].name).toBe('weather');
|
|
200
221
|
});
|
|
222
|
+
it('paginates through all pages of tools', async () => {
|
|
223
|
+
sdkClientMock.listTools
|
|
224
|
+
.mockResolvedValueOnce({
|
|
225
|
+
tools: [{ name: 'tool_a', description: 'A', inputSchema: {} }],
|
|
226
|
+
nextCursor: 'page2',
|
|
227
|
+
})
|
|
228
|
+
.mockResolvedValueOnce({
|
|
229
|
+
tools: [{ name: 'tool_b', description: 'B', inputSchema: {} }],
|
|
230
|
+
nextCursor: 'page3',
|
|
231
|
+
})
|
|
232
|
+
.mockResolvedValueOnce({
|
|
233
|
+
tools: [{ name: 'tool_c', description: 'C', inputSchema: {} }],
|
|
234
|
+
});
|
|
235
|
+
const tools = await client.listTools();
|
|
236
|
+
expect(tools).toHaveLength(3);
|
|
237
|
+
expect(tools.map((t) => t.name)).toEqual(['tool_a', 'tool_b', 'tool_c']);
|
|
238
|
+
expect(sdkClientMock.listTools).toHaveBeenCalledTimes(3);
|
|
239
|
+
expect(sdkClientMock.listTools).toHaveBeenNthCalledWith(1, undefined);
|
|
240
|
+
expect(sdkClientMock.listTools).toHaveBeenNthCalledWith(2, { cursor: 'page2' });
|
|
241
|
+
expect(sdkClientMock.listTools).toHaveBeenNthCalledWith(3, { cursor: 'page3' });
|
|
242
|
+
});
|
|
243
|
+
it('generates description fallback when description is missing', async () => {
|
|
244
|
+
sdkClientMock.listTools.mockResolvedValue({
|
|
245
|
+
tools: [{ name: 'my_tool', inputSchema: {} }],
|
|
246
|
+
});
|
|
247
|
+
const tools = await client.listTools();
|
|
248
|
+
expect(tools[0].description).toBe('Tool which performs my_tool');
|
|
249
|
+
});
|
|
250
|
+
it('generates description fallback when description is empty string', async () => {
|
|
251
|
+
sdkClientMock.listTools.mockResolvedValue({
|
|
252
|
+
tools: [{ name: 'my_tool', description: '', inputSchema: {} }],
|
|
253
|
+
});
|
|
254
|
+
const tools = await client.listTools();
|
|
255
|
+
expect(tools[0].description).toBe('Tool which performs my_tool');
|
|
256
|
+
});
|
|
201
257
|
it('uses callTool when tasksConfig is undefined (default)', async () => {
|
|
202
258
|
const tool = new McpTool({ name: 'calc', description: '', inputSchema: {}, client });
|
|
203
259
|
sdkClientMock.callTool.mockResolvedValue({ content: [] });
|
|
204
260
|
await client.callTool(tool, { op: 'add' });
|
|
205
261
|
expect(sdkClientMock.connect).toHaveBeenCalled();
|
|
206
|
-
expect(sdkClientMock.callTool).toHaveBeenCalledWith({
|
|
207
|
-
name: 'calc',
|
|
208
|
-
arguments: { op: 'add' },
|
|
209
|
-
});
|
|
262
|
+
expect(sdkClientMock.callTool).toHaveBeenCalledWith({ name: 'calc', arguments: { op: 'add' } }, undefined, undefined);
|
|
210
263
|
expect(sdkClientMock.experimental.tasks.callToolStream).not.toHaveBeenCalled();
|
|
211
264
|
});
|
|
265
|
+
it('forwards abort signal to SDK callTool', async () => {
|
|
266
|
+
const tool = new McpTool({ name: 'calc', description: '', inputSchema: {}, client });
|
|
267
|
+
sdkClientMock.callTool.mockResolvedValue({ content: [] });
|
|
268
|
+
const controller = new AbortController();
|
|
269
|
+
await client.callTool(tool, { op: 'add' }, { signal: controller.signal });
|
|
270
|
+
expect(sdkClientMock.callTool).toHaveBeenCalledWith({ name: 'calc', arguments: { op: 'add' } }, undefined, {
|
|
271
|
+
signal: controller.signal,
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
it('forwards abort signal to callToolStream when tasksConfig is provided', async () => {
|
|
275
|
+
const resultsLengthBefore = vi.mocked(Client).mock.results.length;
|
|
276
|
+
const taskClient = new McpClient({
|
|
277
|
+
applicationName: 'TestApp',
|
|
278
|
+
transport: mockTransport,
|
|
279
|
+
tasksConfig: {},
|
|
280
|
+
});
|
|
281
|
+
const taskSdkClientMock = vi.mocked(Client).mock.results[resultsLengthBefore].value;
|
|
282
|
+
const tool = new McpTool({ name: 'calc', description: '', inputSchema: {}, client: taskClient });
|
|
283
|
+
taskSdkClientMock.experimental.tasks.callToolStream.mockReturnValue(createMockCallToolStream({ content: [] })());
|
|
284
|
+
const controller = new AbortController();
|
|
285
|
+
await taskClient.callTool(tool, { op: 'add' }, { signal: controller.signal });
|
|
286
|
+
expect(taskSdkClientMock.experimental.tasks.callToolStream).toHaveBeenCalledWith({ name: 'calc', arguments: { op: 'add' } }, undefined, { timeout: 60000, maxTotalTimeout: 300000, resetTimeoutOnProgress: true, signal: controller.signal });
|
|
287
|
+
});
|
|
212
288
|
it('uses callToolStream when tasksConfig is provided (empty object)', async () => {
|
|
213
289
|
const resultsLengthBefore = vi.mocked(Client).mock.results.length;
|
|
214
290
|
const taskClient = new McpClient({
|
|
@@ -246,6 +322,11 @@ describe('MCP Integration', () => {
|
|
|
246
322
|
expect(sdkClientMock.close).toHaveBeenCalled();
|
|
247
323
|
expect(mockTransport.close).toHaveBeenCalled();
|
|
248
324
|
});
|
|
325
|
+
it('supports Symbol.asyncDispose for await using pattern', async () => {
|
|
326
|
+
await client[Symbol.asyncDispose]();
|
|
327
|
+
expect(sdkClientMock.close).toHaveBeenCalled();
|
|
328
|
+
expect(mockTransport.close).toHaveBeenCalled();
|
|
329
|
+
});
|
|
249
330
|
it('registers elicitation handler before connecting when callback is provided', async () => {
|
|
250
331
|
const resultsLengthBefore = vi.mocked(Client).mock.results.length;
|
|
251
332
|
const callback = vi.fn();
|
|
@@ -273,7 +354,7 @@ describe('MCP Integration', () => {
|
|
|
273
354
|
elicitationCallback: callback,
|
|
274
355
|
});
|
|
275
356
|
const lastCall = vi.mocked(Client).mock.calls.at(-1);
|
|
276
|
-
expect(lastCall[1]).toEqual({ capabilities: { elicitation: { form: {}, url: {} } } });
|
|
357
|
+
expect(lastCall[1]).toEqual(expect.objectContaining({ capabilities: { elicitation: { form: {}, url: {} } } }));
|
|
277
358
|
});
|
|
278
359
|
it('elicitation handler returns accepted result with content', async () => {
|
|
279
360
|
const callbackResult = { action: 'accept', content: { username: 'alice' } };
|
|
@@ -329,6 +410,98 @@ describe('MCP Integration', () => {
|
|
|
329
410
|
await expect(handler(request, extra)).rejects.toThrow('User cancelled');
|
|
330
411
|
});
|
|
331
412
|
});
|
|
413
|
+
describe('tools list changed', () => {
|
|
414
|
+
let client;
|
|
415
|
+
let sdkClientMock;
|
|
416
|
+
beforeEach(() => {
|
|
417
|
+
client = new McpClient({ applicationName: 'TestApp', transport: mockTransport });
|
|
418
|
+
sdkClientMock = vi.mocked(Client).mock.results.at(-1).value;
|
|
419
|
+
sdkClientMock.connect.mockResolvedValue(undefined);
|
|
420
|
+
});
|
|
421
|
+
function triggerToolsChanged() {
|
|
422
|
+
const ctorCall = vi.mocked(Client).mock.calls.at(-1);
|
|
423
|
+
ctorCall[1].listChanged.tools.onChanged(null, null);
|
|
424
|
+
}
|
|
425
|
+
it('calls onToolsChanged with old names and new tools when list changes', async () => {
|
|
426
|
+
sdkClientMock.listTools.mockResolvedValue({
|
|
427
|
+
tools: [{ name: 'tool_a', description: 'A', inputSchema: {} }],
|
|
428
|
+
});
|
|
429
|
+
await client.listTools();
|
|
430
|
+
const onToolsChanged = vi.fn();
|
|
431
|
+
client.onToolsChanged = onToolsChanged;
|
|
432
|
+
sdkClientMock.listTools.mockResolvedValue({
|
|
433
|
+
tools: [
|
|
434
|
+
{ name: 'tool_a', description: 'A', inputSchema: {} },
|
|
435
|
+
{ name: 'tool_b', description: 'B', inputSchema: {} },
|
|
436
|
+
],
|
|
437
|
+
});
|
|
438
|
+
triggerToolsChanged();
|
|
439
|
+
await vi.waitFor(() => expect(onToolsChanged).toHaveBeenCalled());
|
|
440
|
+
expect(onToolsChanged).toHaveBeenCalledWith(['tool_a'], expect.any(Array));
|
|
441
|
+
const newTools = onToolsChanged.mock.calls[0][1];
|
|
442
|
+
expect(newTools.map((t) => t.name)).toEqual(['tool_a', 'tool_b']);
|
|
443
|
+
});
|
|
444
|
+
it('updates registered tool names after each listTools call', async () => {
|
|
445
|
+
sdkClientMock.listTools.mockResolvedValue({
|
|
446
|
+
tools: [
|
|
447
|
+
{ name: 'x', description: 'X', inputSchema: {} },
|
|
448
|
+
{ name: 'y', description: 'Y', inputSchema: {} },
|
|
449
|
+
],
|
|
450
|
+
});
|
|
451
|
+
await client.listTools();
|
|
452
|
+
const onToolsChanged = vi.fn();
|
|
453
|
+
client.onToolsChanged = onToolsChanged;
|
|
454
|
+
sdkClientMock.listTools.mockResolvedValue({
|
|
455
|
+
tools: [{ name: 'z', description: 'Z', inputSchema: {} }],
|
|
456
|
+
});
|
|
457
|
+
triggerToolsChanged();
|
|
458
|
+
await vi.waitFor(() => expect(onToolsChanged).toHaveBeenCalled());
|
|
459
|
+
expect(onToolsChanged).toHaveBeenCalledWith(['x', 'y'], expect.any(Array));
|
|
460
|
+
const newTools = onToolsChanged.mock.calls[0][1];
|
|
461
|
+
expect(newTools.map((t) => t.name)).toEqual(['z']);
|
|
462
|
+
});
|
|
463
|
+
it('does not throw when onToolsChanged is not set', async () => {
|
|
464
|
+
sdkClientMock.listTools.mockResolvedValue({
|
|
465
|
+
tools: [{ name: 'tool_a', description: 'A', inputSchema: {} }],
|
|
466
|
+
});
|
|
467
|
+
await client.listTools();
|
|
468
|
+
sdkClientMock.listTools.mockResolvedValue({
|
|
469
|
+
tools: [{ name: 'tool_b', description: 'B', inputSchema: {} }],
|
|
470
|
+
});
|
|
471
|
+
triggerToolsChanged();
|
|
472
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
473
|
+
});
|
|
474
|
+
it('logs warning and preserves registry when listTools fails during refresh', async () => {
|
|
475
|
+
sdkClientMock.listTools.mockResolvedValue({
|
|
476
|
+
tools: [{ name: 'tool_a', description: 'A', inputSchema: {} }],
|
|
477
|
+
});
|
|
478
|
+
await client.listTools();
|
|
479
|
+
const onToolsChanged = vi.fn();
|
|
480
|
+
client.onToolsChanged = onToolsChanged;
|
|
481
|
+
sdkClientMock.listTools.mockRejectedValue(new Error('server disconnected'));
|
|
482
|
+
const warnSpy = vi.spyOn(logger, 'warn');
|
|
483
|
+
triggerToolsChanged();
|
|
484
|
+
await vi.waitFor(() => expect(warnSpy).toHaveBeenCalled());
|
|
485
|
+
expect(onToolsChanged).not.toHaveBeenCalled();
|
|
486
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('failed to refresh tools'));
|
|
487
|
+
});
|
|
488
|
+
it('coalesces notifications received during an in-flight refresh into one extra refresh', async () => {
|
|
489
|
+
sdkClientMock.listTools.mockResolvedValue({
|
|
490
|
+
tools: [{ name: 'tool_a', description: 'A', inputSchema: {} }],
|
|
491
|
+
});
|
|
492
|
+
await client.listTools();
|
|
493
|
+
const onToolsChanged = vi.fn();
|
|
494
|
+
client.onToolsChanged = onToolsChanged;
|
|
495
|
+
let resolveListTools;
|
|
496
|
+
sdkClientMock.listTools.mockReturnValue(new Promise((r) => (resolveListTools = r)));
|
|
497
|
+
triggerToolsChanged();
|
|
498
|
+
triggerToolsChanged();
|
|
499
|
+
triggerToolsChanged();
|
|
500
|
+
resolveListTools({ tools: [{ name: 'tool_b', description: 'B', inputSchema: {} }] });
|
|
501
|
+
await vi.waitFor(() => expect(onToolsChanged).toHaveBeenCalledTimes(2));
|
|
502
|
+
expect(sdkClientMock.listTools).toHaveBeenCalledTimes(3);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
332
505
|
describe('McpTool', () => {
|
|
333
506
|
const mockClientWrapper = { callTool: vi.fn() };
|
|
334
507
|
const tool = new McpTool({
|
|
@@ -339,9 +512,21 @@ describe('MCP Integration', () => {
|
|
|
339
512
|
});
|
|
340
513
|
const toolContext = {
|
|
341
514
|
toolUse: { toolUseId: 'id-123', name: 'weather', input: { city: 'NYC' } },
|
|
342
|
-
agent: {},
|
|
515
|
+
agent: { cancelSignal: new AbortController().signal },
|
|
343
516
|
invocationState: {},
|
|
517
|
+
interrupt: () => {
|
|
518
|
+
throw new Error('interrupt not available in mock context');
|
|
519
|
+
},
|
|
344
520
|
};
|
|
521
|
+
it('forwards agent cancelSignal to callTool', async () => {
|
|
522
|
+
vi.mocked(mockClientWrapper.callTool).mockResolvedValue({
|
|
523
|
+
content: [{ type: 'text', text: 'ok' }],
|
|
524
|
+
});
|
|
525
|
+
await runTool(tool.stream(toolContext));
|
|
526
|
+
expect(mockClientWrapper.callTool).toHaveBeenCalledWith(tool, { city: 'NYC' }, {
|
|
527
|
+
signal: toolContext.agent.cancelSignal,
|
|
528
|
+
});
|
|
529
|
+
});
|
|
345
530
|
it('returns text results on success', async () => {
|
|
346
531
|
vi.mocked(mockClientWrapper.callTool).mockResolvedValue({
|
|
347
532
|
content: [{ type: 'text', text: 'Sunny' }],
|
|
@@ -560,4 +745,259 @@ describe('MCP Integration', () => {
|
|
|
560
745
|
});
|
|
561
746
|
});
|
|
562
747
|
});
|
|
748
|
+
describe('server metadata getters', () => {
|
|
749
|
+
let client;
|
|
750
|
+
let sdkClientMock;
|
|
751
|
+
beforeEach(() => {
|
|
752
|
+
vi.clearAllMocks();
|
|
753
|
+
client = new McpClient({ applicationName: 'TestApp', transport: mockTransport });
|
|
754
|
+
sdkClientMock = vi.mocked(Client).mock.results.at(-1).value;
|
|
755
|
+
});
|
|
756
|
+
afterEach(() => {
|
|
757
|
+
vi.restoreAllMocks();
|
|
758
|
+
});
|
|
759
|
+
it('returns undefined for all getters before connect', () => {
|
|
760
|
+
sdkClientMock.getServerCapabilities.mockReturnValue(undefined);
|
|
761
|
+
sdkClientMock.getServerVersion.mockReturnValue(undefined);
|
|
762
|
+
sdkClientMock.getInstructions.mockReturnValue(undefined);
|
|
763
|
+
expect(client.serverCapabilities).toBeUndefined();
|
|
764
|
+
expect(client.serverVersion).toBeUndefined();
|
|
765
|
+
expect(client.serverInstructions).toBeUndefined();
|
|
766
|
+
});
|
|
767
|
+
it('returns serverCapabilities after connect', async () => {
|
|
768
|
+
const caps = { tools: {} };
|
|
769
|
+
sdkClientMock.getServerCapabilities.mockReturnValue(caps);
|
|
770
|
+
await client.connect();
|
|
771
|
+
expect(client.serverCapabilities).toBe(caps);
|
|
772
|
+
});
|
|
773
|
+
it('returns serverVersion after connect', async () => {
|
|
774
|
+
const version = { name: 'my-server', version: '1.2.3' };
|
|
775
|
+
sdkClientMock.getServerVersion.mockReturnValue(version);
|
|
776
|
+
await client.connect();
|
|
777
|
+
expect(client.serverVersion).toBe(version);
|
|
778
|
+
});
|
|
779
|
+
it('returns serverInstructions after connect', async () => {
|
|
780
|
+
sdkClientMock.getInstructions.mockReturnValue('Use this server for X.');
|
|
781
|
+
await client.connect();
|
|
782
|
+
expect(client.serverInstructions).toBe('Use this server for X.');
|
|
783
|
+
});
|
|
784
|
+
it('connectionState is disconnected before connect', () => {
|
|
785
|
+
expect(client.connectionState).toBe('disconnected');
|
|
786
|
+
});
|
|
787
|
+
it('connectionState is connected after successful connect', async () => {
|
|
788
|
+
await client.connect();
|
|
789
|
+
expect(client.connectionState).toBe('connected');
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
describe('failOpen', () => {
|
|
793
|
+
let sdkClientMock;
|
|
794
|
+
beforeEach(() => {
|
|
795
|
+
vi.clearAllMocks();
|
|
796
|
+
});
|
|
797
|
+
afterEach(() => {
|
|
798
|
+
vi.restoreAllMocks();
|
|
799
|
+
});
|
|
800
|
+
it('throws on connection failure by default', async () => {
|
|
801
|
+
const client = new McpClient({ applicationName: 'TestApp', transport: mockTransport });
|
|
802
|
+
sdkClientMock = vi.mocked(Client).mock.results.at(-1).value;
|
|
803
|
+
sdkClientMock.connect.mockRejectedValue(new Error('connection refused'));
|
|
804
|
+
await expect(client.connect()).rejects.toThrow('connection refused');
|
|
805
|
+
});
|
|
806
|
+
it('swallows connection failure when failOpen is true', async () => {
|
|
807
|
+
const client = new McpClient({ applicationName: 'TestApp', transport: mockTransport, failOpen: true });
|
|
808
|
+
sdkClientMock = vi.mocked(Client).mock.results.at(-1).value;
|
|
809
|
+
sdkClientMock.connect.mockRejectedValue(new Error('connection refused'));
|
|
810
|
+
await expect(client.connect()).resolves.toBeUndefined();
|
|
811
|
+
});
|
|
812
|
+
it('logs a warning when failOpen swallows a connection failure', async () => {
|
|
813
|
+
const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => { });
|
|
814
|
+
const client = new McpClient({ applicationName: 'TestApp', transport: mockTransport, failOpen: true });
|
|
815
|
+
sdkClientMock = vi.mocked(Client).mock.results.at(-1).value;
|
|
816
|
+
sdkClientMock.connect.mockRejectedValue(new Error('connection refused'));
|
|
817
|
+
await client.connect();
|
|
818
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('MCP server failed to connect'));
|
|
819
|
+
});
|
|
820
|
+
it('listTools returns empty array when failOpen and connection failed', async () => {
|
|
821
|
+
const client = new McpClient({ applicationName: 'TestApp', transport: mockTransport, failOpen: true });
|
|
822
|
+
sdkClientMock = vi.mocked(Client).mock.results.at(-1).value;
|
|
823
|
+
sdkClientMock.connect.mockRejectedValue(new Error('connection refused'));
|
|
824
|
+
const tools = await client.listTools();
|
|
825
|
+
expect(tools).toEqual([]);
|
|
826
|
+
});
|
|
827
|
+
it('callTool throws when failOpen and connection failed', async () => {
|
|
828
|
+
const client = new McpClient({ applicationName: 'TestApp', transport: mockTransport, failOpen: true });
|
|
829
|
+
sdkClientMock = vi.mocked(Client).mock.results.at(-1).value;
|
|
830
|
+
sdkClientMock.connect.mockRejectedValue(new Error('connection refused'));
|
|
831
|
+
const tool = new McpTool({ name: 'my_tool', description: '', inputSchema: {}, client });
|
|
832
|
+
await expect(client.callTool(tool, {})).rejects.toThrow('MCP server failed to connect. Call connect(true) to retry.');
|
|
833
|
+
});
|
|
834
|
+
it('does not retry connection on subsequent calls after failOpen failure', async () => {
|
|
835
|
+
const client = new McpClient({ applicationName: 'TestApp', transport: mockTransport, failOpen: true });
|
|
836
|
+
sdkClientMock = vi.mocked(Client).mock.results.at(-1).value;
|
|
837
|
+
sdkClientMock.connect.mockRejectedValue(new Error('connection refused'));
|
|
838
|
+
await client.listTools();
|
|
839
|
+
await client.listTools();
|
|
840
|
+
expect(sdkClientMock.connect).toHaveBeenCalledTimes(1);
|
|
841
|
+
});
|
|
842
|
+
it('recovers after explicit connect(true) when server comes back', async () => {
|
|
843
|
+
const client = new McpClient({ applicationName: 'TestApp', transport: mockTransport, failOpen: true });
|
|
844
|
+
sdkClientMock = vi.mocked(Client).mock.results.at(-1).value;
|
|
845
|
+
sdkClientMock.connect.mockRejectedValueOnce(new Error('connection refused'));
|
|
846
|
+
sdkClientMock.listTools.mockResolvedValue({ tools: [] });
|
|
847
|
+
const firstTools = await client.listTools();
|
|
848
|
+
expect(firstTools).toEqual([]);
|
|
849
|
+
expect(client.connectionState).toBe('failed');
|
|
850
|
+
await client.connect(true);
|
|
851
|
+
const secondTools = await client.listTools();
|
|
852
|
+
expect(secondTools).toEqual([]);
|
|
853
|
+
expect(client.connectionState).toBe('connected');
|
|
854
|
+
expect(sdkClientMock.connect).toHaveBeenCalledTimes(2);
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
describe('log routing', () => {
|
|
858
|
+
let notificationHandler;
|
|
859
|
+
let sdkClientMock;
|
|
860
|
+
beforeEach(() => {
|
|
861
|
+
vi.clearAllMocks();
|
|
862
|
+
new McpClient({ applicationName: 'TestApp', transport: mockTransport });
|
|
863
|
+
sdkClientMock = vi.mocked(Client).mock.results.at(-1).value;
|
|
864
|
+
// Handler is registered in the constructor — read it from the first setNotificationHandler call
|
|
865
|
+
notificationHandler = sdkClientMock.setNotificationHandler.mock.calls[0][1];
|
|
866
|
+
});
|
|
867
|
+
afterEach(() => {
|
|
868
|
+
vi.restoreAllMocks();
|
|
869
|
+
});
|
|
870
|
+
it('routes debug level to logger.debug', () => {
|
|
871
|
+
const spy = vi.spyOn(logger, 'debug').mockImplementation(() => { });
|
|
872
|
+
notificationHandler({ params: { level: 'debug', data: 'hello' } });
|
|
873
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('hello'));
|
|
874
|
+
});
|
|
875
|
+
it('routes info level to logger.info', () => {
|
|
876
|
+
const spy = vi.spyOn(logger, 'info').mockImplementation(() => { });
|
|
877
|
+
notificationHandler({ params: { level: 'info', data: 'hello' } });
|
|
878
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('hello'));
|
|
879
|
+
});
|
|
880
|
+
it('routes notice level to logger.info', () => {
|
|
881
|
+
const spy = vi.spyOn(logger, 'info').mockImplementation(() => { });
|
|
882
|
+
notificationHandler({ params: { level: 'notice', data: 'hello' } });
|
|
883
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('hello'));
|
|
884
|
+
});
|
|
885
|
+
it('routes warning level to logger.warn', () => {
|
|
886
|
+
const spy = vi.spyOn(logger, 'warn').mockImplementation(() => { });
|
|
887
|
+
notificationHandler({ params: { level: 'warning', data: 'hello' } });
|
|
888
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('hello'));
|
|
889
|
+
});
|
|
890
|
+
it('routes error level to logger.error', () => {
|
|
891
|
+
const spy = vi.spyOn(logger, 'error').mockImplementation(() => { });
|
|
892
|
+
notificationHandler({ params: { level: 'error', data: 'hello' } });
|
|
893
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('hello'));
|
|
894
|
+
});
|
|
895
|
+
it('routes critical level to logger.error', () => {
|
|
896
|
+
const spy = vi.spyOn(logger, 'error').mockImplementation(() => { });
|
|
897
|
+
notificationHandler({ params: { level: 'critical', data: 'hello' } });
|
|
898
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('hello'));
|
|
899
|
+
});
|
|
900
|
+
it('routes alert level to logger.error', () => {
|
|
901
|
+
const spy = vi.spyOn(logger, 'error').mockImplementation(() => { });
|
|
902
|
+
notificationHandler({ params: { level: 'alert', data: 'hello' } });
|
|
903
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('hello'));
|
|
904
|
+
});
|
|
905
|
+
it('routes emergency level to logger.error', () => {
|
|
906
|
+
const spy = vi.spyOn(logger, 'error').mockImplementation(() => { });
|
|
907
|
+
notificationHandler({ params: { level: 'emergency', data: 'hello' } });
|
|
908
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('hello'));
|
|
909
|
+
});
|
|
910
|
+
it('includes logger name and data in the message', () => {
|
|
911
|
+
const spy = vi.spyOn(logger, 'info').mockImplementation(() => { });
|
|
912
|
+
notificationHandler({ params: { level: 'info', logger: 'my-server', data: { key: 'val' } } });
|
|
913
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('my-server'));
|
|
914
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('key'));
|
|
915
|
+
});
|
|
916
|
+
it('calls custom logHandler when provided', () => {
|
|
917
|
+
const customHandler = vi.fn();
|
|
918
|
+
new McpClient({ applicationName: 'TestApp', transport: mockTransport, logHandler: customHandler });
|
|
919
|
+
const customSdkMock = vi.mocked(Client).mock.results.at(-1).value;
|
|
920
|
+
const capturedHandler = customSdkMock.setNotificationHandler.mock.calls[0][1];
|
|
921
|
+
const params = { level: 'info', data: 'test' };
|
|
922
|
+
capturedHandler({ params });
|
|
923
|
+
expect(customHandler).toHaveBeenCalledWith(params);
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
describe('McpClient transport resolution', () => {
|
|
927
|
+
beforeEach(() => {
|
|
928
|
+
vi.clearAllMocks();
|
|
929
|
+
});
|
|
930
|
+
it('constructs StreamableHTTPClientTransport when url is provided', () => {
|
|
931
|
+
new McpClient({ url: 'https://mcp.example.com' });
|
|
932
|
+
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(new URL('https://mcp.example.com'), undefined);
|
|
933
|
+
});
|
|
934
|
+
it('constructs ClientCredentialsProvider when auth is provided', () => {
|
|
935
|
+
new McpClient({ url: 'https://mcp.example.com', auth: { clientId: 'id', clientSecret: 'secret' } });
|
|
936
|
+
expect(ClientCredentialsProvider).toHaveBeenCalledWith({ clientId: 'id', clientSecret: 'secret' });
|
|
937
|
+
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(new URL('https://mcp.example.com'), {
|
|
938
|
+
authProvider: expect.anything(),
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
it('passes scopes as space-separated string', () => {
|
|
942
|
+
new McpClient({
|
|
943
|
+
url: 'https://mcp.example.com',
|
|
944
|
+
auth: { clientId: 'id', clientSecret: 'secret', scopes: ['read', 'write'] },
|
|
945
|
+
});
|
|
946
|
+
expect(ClientCredentialsProvider).toHaveBeenCalledWith({
|
|
947
|
+
clientId: 'id',
|
|
948
|
+
clientSecret: 'secret',
|
|
949
|
+
scope: 'read write',
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
it('passes custom authProvider to transport', () => {
|
|
953
|
+
const customProvider = { redirectUrl: undefined, clientMetadata: {} };
|
|
954
|
+
new McpClient({ url: 'https://mcp.example.com', authProvider: customProvider });
|
|
955
|
+
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(new URL('https://mcp.example.com'), {
|
|
956
|
+
authProvider: customProvider,
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
it('throws when both transport and url are provided', () => {
|
|
960
|
+
expect(() => new McpClient({ transport: mockTransport, url: 'https://mcp.example.com' })).toThrow('provide either "transport" or "url", not both');
|
|
961
|
+
});
|
|
962
|
+
it('throws when neither transport nor url is provided', () => {
|
|
963
|
+
expect(() => new McpClient({})).toThrow('either "transport" or "url" must be provided');
|
|
964
|
+
});
|
|
965
|
+
it('throws when auth is provided with transport', () => {
|
|
966
|
+
expect(() => new McpClient({ transport: mockTransport, auth: { clientId: 'x', clientSecret: 'y' } })).toThrow('"auth", "authProvider", and "headers" require "url"');
|
|
967
|
+
});
|
|
968
|
+
it('throws when both auth and authProvider are provided', () => {
|
|
969
|
+
const customProvider = {};
|
|
970
|
+
expect(() => new McpClient({
|
|
971
|
+
url: 'https://mcp.example.com',
|
|
972
|
+
auth: { clientId: 'x', clientSecret: 'y' },
|
|
973
|
+
authProvider: customProvider,
|
|
974
|
+
})).toThrow('provide either "auth" or "authProvider", not both');
|
|
975
|
+
});
|
|
976
|
+
it('accepts URL instance for url field', () => {
|
|
977
|
+
const url = new URL('https://mcp.example.com/path');
|
|
978
|
+
new McpClient({ url });
|
|
979
|
+
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(url, undefined);
|
|
980
|
+
});
|
|
981
|
+
it('passes headers as requestInit to transport', () => {
|
|
982
|
+
new McpClient({ url: 'https://mcp.example.com', headers: { 'X-Api-Key': 'abc' } });
|
|
983
|
+
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(new URL('https://mcp.example.com'), {
|
|
984
|
+
requestInit: { headers: { 'X-Api-Key': 'abc' } },
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
it('passes both auth and headers to transport', () => {
|
|
988
|
+
new McpClient({
|
|
989
|
+
url: 'https://mcp.example.com',
|
|
990
|
+
auth: { clientId: 'id', clientSecret: 'secret' },
|
|
991
|
+
headers: { 'X-Trace': '123' },
|
|
992
|
+
});
|
|
993
|
+
expect(ClientCredentialsProvider).toHaveBeenCalledWith({ clientId: 'id', clientSecret: 'secret' });
|
|
994
|
+
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(new URL('https://mcp.example.com'), {
|
|
995
|
+
authProvider: expect.anything(),
|
|
996
|
+
requestInit: { headers: { 'X-Trace': '123' } },
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
it('throws when headers is provided with transport', () => {
|
|
1000
|
+
expect(() => new McpClient({ transport: mockTransport, headers: { 'X-Foo': 'bar' } })).toThrow('"auth", "authProvider", and "headers" require "url"');
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
563
1003
|
//# sourceMappingURL=mcp.test.js.map
|