@decocms/mesh-sdk 1.2.1 → 1.2.2
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 +6 -4
- package/src/context/index.ts +6 -1
- package/src/context/project-context.tsx +78 -29
- package/src/hooks/index.ts +7 -0
- package/src/hooks/use-collections.ts +160 -51
- package/src/hooks/use-connection.ts +39 -4
- package/src/hooks/use-mcp-client.ts +55 -2
- package/src/hooks/use-mcp-prompts.ts +16 -6
- package/src/hooks/use-mcp-resources.ts +15 -5
- package/src/index.ts +82 -3
- package/src/lib/bridge-transport.test.ts +368 -0
- package/src/lib/bridge-transport.ts +434 -0
- package/src/lib/constants.ts +113 -10
- package/src/lib/default-model.ts +96 -0
- package/src/lib/mcp-oauth.ts +80 -9
- package/src/lib/query-keys.ts +1 -0
- package/src/lib/server-client-bridge.ts +146 -0
- package/src/lib/usage.test.ts +163 -0
- package/src/lib/usage.ts +161 -0
- package/src/plugins/index.ts +15 -0
- package/src/plugins/plugin-context-provider.tsx +99 -0
- package/src/plugins/topbar-portal.tsx +118 -0
- package/src/types/ai-providers.ts +68 -0
- package/src/types/connection.ts +38 -20
- package/src/types/decopilot-events.ts +128 -0
- package/src/types/index.ts +30 -1
- package/src/types/virtual-mcp.ts +107 -109
|
@@ -21,6 +21,12 @@ export interface CreateMcpClientOptions {
|
|
|
21
21
|
|
|
22
22
|
export type UseMcpClientOptions = CreateMcpClientOptions;
|
|
23
23
|
|
|
24
|
+
export interface UseMcpClientOptionalOptions
|
|
25
|
+
extends Omit<CreateMcpClientOptions, "connectionId"> {
|
|
26
|
+
/** Connection ID - string for connection MCP, null for default/self, undefined to skip (returns null) */
|
|
27
|
+
connectionId: string | null | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
/**
|
|
25
31
|
* Build the MCP URL from connectionId and optional meshUrl
|
|
26
32
|
* Uses /mcp/:connectionId for all servers
|
|
@@ -89,6 +95,7 @@ export async function createMCPClient({
|
|
|
89
95
|
token ?? "",
|
|
90
96
|
meshUrl ?? "",
|
|
91
97
|
);
|
|
98
|
+
|
|
92
99
|
(client as Client & { toJSON: () => string }).toJSON = () =>
|
|
93
100
|
`mcp-client:${queryKey.join(":")}`;
|
|
94
101
|
|
|
@@ -110,7 +117,7 @@ export function useMCPClient({
|
|
|
110
117
|
}: UseMcpClientOptions): Client {
|
|
111
118
|
const queryKey = KEYS.mcpClient(
|
|
112
119
|
orgId,
|
|
113
|
-
connectionId ?? "",
|
|
120
|
+
connectionId ?? "self",
|
|
114
121
|
token ?? "",
|
|
115
122
|
meshUrl ?? "",
|
|
116
123
|
);
|
|
@@ -122,6 +129,52 @@ export function useMCPClient({
|
|
|
122
129
|
gcTime: 0, // Clean up immediately when query is inactive
|
|
123
130
|
});
|
|
124
131
|
|
|
125
|
-
// useSuspenseQuery guarantees data is available (suspends until ready)
|
|
126
132
|
return client!;
|
|
127
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Optional MCP client - returns null when connectionId is undefined (skip creating client).
|
|
137
|
+
* Use when the connection may not be selected yet (e.g. model picker with no connections).
|
|
138
|
+
*
|
|
139
|
+
* - connectionId: string → connection-specific MCP
|
|
140
|
+
* - connectionId: null → default/self MCP
|
|
141
|
+
* - connectionId: undefined → skip (returns null, no MCP call)
|
|
142
|
+
*
|
|
143
|
+
* @param options - Configuration for the MCP client
|
|
144
|
+
* @returns The MCP client instance, or null when connectionId is undefined
|
|
145
|
+
*/
|
|
146
|
+
export function useMCPClientOptional({
|
|
147
|
+
connectionId,
|
|
148
|
+
orgId,
|
|
149
|
+
token,
|
|
150
|
+
meshUrl,
|
|
151
|
+
}: UseMcpClientOptionalOptions): Client | null {
|
|
152
|
+
const queryKey =
|
|
153
|
+
connectionId !== undefined
|
|
154
|
+
? KEYS.mcpClient(
|
|
155
|
+
orgId,
|
|
156
|
+
connectionId ?? "self",
|
|
157
|
+
token ?? "",
|
|
158
|
+
meshUrl ?? "",
|
|
159
|
+
)
|
|
160
|
+
: (["mcp", "client", "skip", orgId] as const);
|
|
161
|
+
|
|
162
|
+
const { data: client } = useSuspenseQuery({
|
|
163
|
+
queryKey,
|
|
164
|
+
queryFn: async () => {
|
|
165
|
+
if (connectionId === undefined) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return createMCPClient({
|
|
169
|
+
connectionId: connectionId as string | null,
|
|
170
|
+
orgId,
|
|
171
|
+
token,
|
|
172
|
+
meshUrl,
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
staleTime: Infinity,
|
|
176
|
+
gcTime: 0,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return client ?? null;
|
|
180
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import {
|
|
3
|
+
ErrorCode,
|
|
4
|
+
McpError,
|
|
5
|
+
type GetPromptRequest,
|
|
6
|
+
type GetPromptResult,
|
|
7
|
+
type ListPromptsResult,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
2
9
|
import {
|
|
3
10
|
useQuery,
|
|
4
11
|
UseQueryResult,
|
|
@@ -7,11 +14,6 @@ import {
|
|
|
7
14
|
type UseQueryOptions,
|
|
8
15
|
type UseSuspenseQueryOptions,
|
|
9
16
|
} from "@tanstack/react-query";
|
|
10
|
-
import type {
|
|
11
|
-
GetPromptRequest,
|
|
12
|
-
GetPromptResult,
|
|
13
|
-
ListPromptsResult,
|
|
14
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
15
17
|
import { KEYS } from "../lib/query-keys";
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -23,7 +25,15 @@ export async function listPrompts(client: Client): Promise<ListPromptsResult> {
|
|
|
23
25
|
if (!capabilities?.prompts) {
|
|
24
26
|
return { prompts: [] };
|
|
25
27
|
}
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
return await client.listPrompts();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error instanceof McpError && error.code === ErrorCode.MethodNotFound) {
|
|
33
|
+
return { prompts: [] };
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
/**
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import {
|
|
3
|
+
ErrorCode,
|
|
4
|
+
McpError,
|
|
5
|
+
type ListResourcesResult,
|
|
6
|
+
type ReadResourceResult,
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
2
8
|
import {
|
|
3
9
|
useQuery,
|
|
4
10
|
useSuspenseQuery,
|
|
@@ -7,10 +13,6 @@ import {
|
|
|
7
13
|
type UseSuspenseQueryOptions,
|
|
8
14
|
type UseSuspenseQueryResult,
|
|
9
15
|
} from "@tanstack/react-query";
|
|
10
|
-
import type {
|
|
11
|
-
ListResourcesResult,
|
|
12
|
-
ReadResourceResult,
|
|
13
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
16
|
import { KEYS } from "../lib/query-keys";
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -24,7 +26,15 @@ export async function listResources(
|
|
|
24
26
|
if (!capabilities?.resources) {
|
|
25
27
|
return { resources: [] };
|
|
26
28
|
}
|
|
27
|
-
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
return await client.listResources();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof McpError && error.code === ErrorCode.MethodNotFound) {
|
|
34
|
+
return { resources: [] };
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
/**
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
export {
|
|
3
3
|
ProjectContextProvider,
|
|
4
4
|
useProjectContext,
|
|
5
|
+
useOrg,
|
|
6
|
+
useCurrentProject,
|
|
7
|
+
useIsOrgAdmin,
|
|
5
8
|
Locator,
|
|
6
|
-
ORG_ADMIN_PROJECT_SLUG,
|
|
7
9
|
type ProjectContextProviderProps,
|
|
8
10
|
type ProjectLocator,
|
|
9
11
|
type LocatorStructured,
|
|
12
|
+
type OrganizationData,
|
|
13
|
+
type ProjectData,
|
|
14
|
+
type ProjectUI,
|
|
10
15
|
} from "./context";
|
|
11
16
|
|
|
12
17
|
// Hooks
|
|
@@ -15,9 +20,14 @@ export {
|
|
|
15
20
|
useCollectionItem,
|
|
16
21
|
useCollectionList,
|
|
17
22
|
useCollectionActions,
|
|
23
|
+
buildWhereExpression,
|
|
24
|
+
buildOrderByExpression,
|
|
25
|
+
buildCollectionQueryKey,
|
|
26
|
+
EMPTY_COLLECTION_LIST_RESULT,
|
|
18
27
|
type CollectionEntity,
|
|
19
28
|
type CollectionFilter,
|
|
20
29
|
type UseCollectionListOptions,
|
|
30
|
+
type CollectionQueryKey,
|
|
21
31
|
// Connection hooks
|
|
22
32
|
useConnections,
|
|
23
33
|
useConnection,
|
|
@@ -27,8 +37,10 @@ export {
|
|
|
27
37
|
// MCP client hook and factory
|
|
28
38
|
createMCPClient,
|
|
29
39
|
useMCPClient,
|
|
40
|
+
useMCPClientOptional,
|
|
30
41
|
type CreateMcpClientOptions,
|
|
31
42
|
type UseMcpClientOptions,
|
|
43
|
+
type UseMcpClientOptionalOptions,
|
|
32
44
|
// MCP tools hooks
|
|
33
45
|
useMCPToolsList,
|
|
34
46
|
useMCPToolsListQuery,
|
|
@@ -68,6 +80,17 @@ export {
|
|
|
68
80
|
|
|
69
81
|
// Types
|
|
70
82
|
export {
|
|
83
|
+
// AI Provider types
|
|
84
|
+
PROVIDER_IDS,
|
|
85
|
+
MODEL_CAPABILITIES,
|
|
86
|
+
type ProviderId,
|
|
87
|
+
type ModelCapability,
|
|
88
|
+
type AiProviderModel,
|
|
89
|
+
type AiProviderModelLimits,
|
|
90
|
+
type AiProviderModelCosts,
|
|
91
|
+
type AiProviderKey,
|
|
92
|
+
type AiProviderInfo,
|
|
93
|
+
// Connection types
|
|
71
94
|
ConnectionEntitySchema,
|
|
72
95
|
ConnectionCreateDataSchema,
|
|
73
96
|
ConnectionUpdateDataSchema,
|
|
@@ -90,20 +113,58 @@ export {
|
|
|
90
113
|
type VirtualMCPCreateData,
|
|
91
114
|
type VirtualMCPUpdateData,
|
|
92
115
|
type VirtualMCPConnection,
|
|
93
|
-
|
|
116
|
+
// Decopilot event types
|
|
117
|
+
THREAD_STATUSES,
|
|
118
|
+
THREAD_DISPLAY_STATUSES,
|
|
119
|
+
DECOPILOT_EVENTS,
|
|
120
|
+
ALL_DECOPILOT_EVENT_TYPES,
|
|
121
|
+
createDecopilotStepEvent,
|
|
122
|
+
createDecopilotFinishEvent,
|
|
123
|
+
createDecopilotThreadStatusEvent,
|
|
124
|
+
type ThreadStatus,
|
|
125
|
+
type ThreadDisplayStatus,
|
|
126
|
+
type DecopilotEventType,
|
|
127
|
+
type DecopilotStepEvent,
|
|
128
|
+
type DecopilotFinishEvent,
|
|
129
|
+
type DecopilotThreadStatusEvent,
|
|
130
|
+
type DecopilotSSEEvent,
|
|
131
|
+
type DecopilotEventMap,
|
|
94
132
|
} from "./types";
|
|
95
133
|
|
|
96
134
|
// Streamable HTTP transport
|
|
97
135
|
export { StreamableHTTPClientTransport } from "./lib/streamable-http-client-transport";
|
|
98
136
|
|
|
137
|
+
// Bridge transport
|
|
138
|
+
export {
|
|
139
|
+
createBridgeTransportPair,
|
|
140
|
+
BridgeClientTransport,
|
|
141
|
+
BridgeServerTransport,
|
|
142
|
+
type BridgeTransportPair,
|
|
143
|
+
} from "./lib/bridge-transport";
|
|
144
|
+
|
|
145
|
+
// Server-client bridge
|
|
146
|
+
export {
|
|
147
|
+
createServerFromClient,
|
|
148
|
+
type ServerFromClientOptions,
|
|
149
|
+
} from "./lib/server-client-bridge";
|
|
150
|
+
|
|
99
151
|
// Query keys
|
|
100
152
|
export { KEYS } from "./lib/query-keys";
|
|
101
153
|
|
|
154
|
+
// Default model selection
|
|
155
|
+
export {
|
|
156
|
+
DEFAULT_MODEL_PREFERENCES,
|
|
157
|
+
FAST_MODEL_PREFERENCES,
|
|
158
|
+
selectDefaultModel,
|
|
159
|
+
getFastModel,
|
|
160
|
+
} from "./lib/default-model";
|
|
161
|
+
|
|
102
162
|
// MCP OAuth utilities
|
|
103
163
|
export {
|
|
104
164
|
authenticateMcp,
|
|
105
165
|
handleOAuthCallback,
|
|
106
166
|
isConnectionAuthenticated,
|
|
167
|
+
setOAuthRedirectOrigin,
|
|
107
168
|
type McpOAuthProviderOptions,
|
|
108
169
|
type OAuthTokenInfo,
|
|
109
170
|
type AuthenticateMcpResult,
|
|
@@ -111,18 +172,36 @@ export {
|
|
|
111
172
|
type OAuthWindowMode,
|
|
112
173
|
} from "./lib/mcp-oauth";
|
|
113
174
|
|
|
175
|
+
// Usage utilities
|
|
176
|
+
export {
|
|
177
|
+
getCostFromUsage,
|
|
178
|
+
emptyUsageStats,
|
|
179
|
+
addUsage,
|
|
180
|
+
calculateUsageStats,
|
|
181
|
+
sanitizeProviderMetadata,
|
|
182
|
+
type UsageData,
|
|
183
|
+
type UsageStats,
|
|
184
|
+
} from "./lib/usage";
|
|
185
|
+
|
|
114
186
|
// Constants and well-known MCP definitions
|
|
115
187
|
export {
|
|
116
188
|
// Frontend self MCP ID
|
|
117
189
|
SELF_MCP_ALIAS_ID,
|
|
190
|
+
// Frontend dev-assets MCP ID
|
|
191
|
+
DEV_ASSETS_MCP_ALIAS_ID,
|
|
118
192
|
// Org-scoped MCP ID generators
|
|
119
193
|
WellKnownOrgMCPId,
|
|
120
194
|
// Connection factory functions
|
|
121
195
|
getWellKnownRegistryConnection,
|
|
122
196
|
getWellKnownCommunityRegistryConnection,
|
|
123
197
|
getWellKnownSelfConnection,
|
|
198
|
+
getWellKnownDevAssetsConnection,
|
|
124
199
|
getWellKnownOpenRouterConnection,
|
|
125
200
|
getWellKnownMcpStudioConnection,
|
|
126
201
|
// Virtual MCP factory functions
|
|
127
|
-
|
|
202
|
+
getWellKnownDecopilotVirtualMCP,
|
|
203
|
+
getWellKnownDecopilotConnection,
|
|
204
|
+
// Decopilot utilities
|
|
205
|
+
isDecopilot,
|
|
206
|
+
getDecopilotId,
|
|
128
207
|
} from "./lib/constants";
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import {
|
|
6
|
+
createBridgeTransportPair,
|
|
7
|
+
BridgeClientTransport,
|
|
8
|
+
BridgeServerTransport,
|
|
9
|
+
} from "./bridge-transport";
|
|
10
|
+
|
|
11
|
+
/** Poll until `condition` returns true, checking every 5ms up to `timeoutMs`. */
|
|
12
|
+
async function waitFor(
|
|
13
|
+
condition: () => boolean,
|
|
14
|
+
timeoutMs = 200,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const start = Date.now();
|
|
17
|
+
while (!condition()) {
|
|
18
|
+
if (Date.now() - start > timeoutMs) return;
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("BridgeTransport", () => {
|
|
24
|
+
describe("createBridgeTransportPair", () => {
|
|
25
|
+
it("should create a pair of transports", () => {
|
|
26
|
+
const { client, server, channel } = createBridgeTransportPair();
|
|
27
|
+
|
|
28
|
+
expect(client).toBeInstanceOf(BridgeClientTransport);
|
|
29
|
+
expect(server).toBeInstanceOf(BridgeServerTransport);
|
|
30
|
+
expect(channel).toBeDefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should create transports with microtask scheduling", () => {
|
|
34
|
+
const { client, server } = createBridgeTransportPair();
|
|
35
|
+
expect(client).toBeDefined();
|
|
36
|
+
expect(server).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("message delivery", () => {
|
|
41
|
+
it("should deliver messages in order client->server", async () => {
|
|
42
|
+
const { client, server } = createBridgeTransportPair();
|
|
43
|
+
|
|
44
|
+
const receivedMessages: JSONRPCMessage[] = [];
|
|
45
|
+
|
|
46
|
+
await server.start();
|
|
47
|
+
server.onmessage = (message) => {
|
|
48
|
+
receivedMessages.push(message);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
await client.start();
|
|
52
|
+
|
|
53
|
+
const msg1: JSONRPCMessage = {
|
|
54
|
+
jsonrpc: "2.0",
|
|
55
|
+
id: 1,
|
|
56
|
+
method: "test",
|
|
57
|
+
params: { foo: "bar" },
|
|
58
|
+
};
|
|
59
|
+
const msg2: JSONRPCMessage = {
|
|
60
|
+
jsonrpc: "2.0",
|
|
61
|
+
id: 2,
|
|
62
|
+
method: "test2",
|
|
63
|
+
params: { baz: "qux" },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
await client.send(msg1);
|
|
67
|
+
await client.send(msg2);
|
|
68
|
+
|
|
69
|
+
// Wait for microtask to process (or sync mode processes immediately)
|
|
70
|
+
await new Promise((resolve) => queueMicrotask(resolve));
|
|
71
|
+
|
|
72
|
+
expect(receivedMessages).toHaveLength(2);
|
|
73
|
+
expect(receivedMessages[0]).toEqual(msg1);
|
|
74
|
+
expect(receivedMessages[1]).toEqual(msg2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should deliver messages in order server->client", async () => {
|
|
78
|
+
const { client, server } = createBridgeTransportPair();
|
|
79
|
+
|
|
80
|
+
const receivedMessages: JSONRPCMessage[] = [];
|
|
81
|
+
|
|
82
|
+
await client.start();
|
|
83
|
+
client.onmessage = (message) => {
|
|
84
|
+
receivedMessages.push(message);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
await server.start();
|
|
88
|
+
|
|
89
|
+
const msg1: JSONRPCMessage = {
|
|
90
|
+
jsonrpc: "2.0",
|
|
91
|
+
id: 1,
|
|
92
|
+
method: "test",
|
|
93
|
+
params: { foo: "bar" },
|
|
94
|
+
};
|
|
95
|
+
const msg2: JSONRPCMessage = {
|
|
96
|
+
jsonrpc: "2.0",
|
|
97
|
+
id: 2,
|
|
98
|
+
method: "test2",
|
|
99
|
+
params: { baz: "qux" },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
await server.send(msg1);
|
|
103
|
+
await server.send(msg2);
|
|
104
|
+
|
|
105
|
+
// Wait for microtask to process (or sync mode processes immediately)
|
|
106
|
+
await new Promise((resolve) => queueMicrotask(resolve));
|
|
107
|
+
|
|
108
|
+
expect(receivedMessages).toHaveLength(2);
|
|
109
|
+
expect(receivedMessages[0]).toEqual(msg1);
|
|
110
|
+
expect(receivedMessages[1]).toEqual(msg2);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should batch multiple messages in a single microtask", async () => {
|
|
114
|
+
const { client, server } = createBridgeTransportPair();
|
|
115
|
+
|
|
116
|
+
const receivedMessages: JSONRPCMessage[] = [];
|
|
117
|
+
let flushCount = 0;
|
|
118
|
+
|
|
119
|
+
await server.start();
|
|
120
|
+
server.onmessage = (message) => {
|
|
121
|
+
receivedMessages.push(message);
|
|
122
|
+
flushCount++;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await client.start();
|
|
126
|
+
|
|
127
|
+
// Send multiple messages synchronously
|
|
128
|
+
await client.send({ jsonrpc: "2.0", id: 1, method: "test1" });
|
|
129
|
+
await client.send({ jsonrpc: "2.0", id: 2, method: "test2" });
|
|
130
|
+
await client.send({ jsonrpc: "2.0", id: 3, method: "test3" });
|
|
131
|
+
|
|
132
|
+
// Wait for microtask to process
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
134
|
+
|
|
135
|
+
expect(receivedMessages).toHaveLength(3);
|
|
136
|
+
// All messages should be delivered in a single flush
|
|
137
|
+
expect(flushCount).toBeGreaterThanOrEqual(1);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("start()", () => {
|
|
142
|
+
it("should allow starting client transport", async () => {
|
|
143
|
+
const { client } = createBridgeTransportPair();
|
|
144
|
+
await expect(client.start()).resolves.toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should allow starting server transport", async () => {
|
|
148
|
+
const { server } = createBridgeTransportPair();
|
|
149
|
+
await expect(server.start()).resolves.toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should throw if started twice", async () => {
|
|
153
|
+
const { client } = createBridgeTransportPair();
|
|
154
|
+
await client.start();
|
|
155
|
+
await expect(client.start()).rejects.toThrow("already started");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("close()", () => {
|
|
160
|
+
it("should close client transport and notify server", async () => {
|
|
161
|
+
const { client, server } = createBridgeTransportPair();
|
|
162
|
+
|
|
163
|
+
await client.start();
|
|
164
|
+
await server.start();
|
|
165
|
+
|
|
166
|
+
let serverClosed = false;
|
|
167
|
+
server.onclose = () => {
|
|
168
|
+
serverClosed = true;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
await client.close();
|
|
172
|
+
|
|
173
|
+
expect(serverClosed).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should close server transport and notify client", async () => {
|
|
177
|
+
const { client, server } = createBridgeTransportPair();
|
|
178
|
+
|
|
179
|
+
await client.start();
|
|
180
|
+
await server.start();
|
|
181
|
+
|
|
182
|
+
let clientClosed = false;
|
|
183
|
+
client.onclose = () => {
|
|
184
|
+
clientClosed = true;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
await server.close();
|
|
188
|
+
|
|
189
|
+
expect(clientClosed).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should prevent sending messages after close", async () => {
|
|
193
|
+
const { client, server } = createBridgeTransportPair();
|
|
194
|
+
|
|
195
|
+
await client.start();
|
|
196
|
+
await server.start();
|
|
197
|
+
|
|
198
|
+
const receivedMessages: JSONRPCMessage[] = [];
|
|
199
|
+
server.onmessage = (msg) => receivedMessages.push(msg);
|
|
200
|
+
|
|
201
|
+
// Send first message and wait for delivery
|
|
202
|
+
await client.send({ jsonrpc: "2.0", id: 1, method: "test" });
|
|
203
|
+
await waitFor(() => receivedMessages.length >= 1);
|
|
204
|
+
|
|
205
|
+
expect(receivedMessages.length).toBeGreaterThanOrEqual(1);
|
|
206
|
+
const initialCount = receivedMessages.length;
|
|
207
|
+
|
|
208
|
+
// Close client
|
|
209
|
+
await client.close();
|
|
210
|
+
|
|
211
|
+
// Try to send after close (should be silent no-op)
|
|
212
|
+
await client.send({ jsonrpc: "2.0", id: 2, method: "test2" });
|
|
213
|
+
|
|
214
|
+
// Wait a bit to ensure no delivery
|
|
215
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
216
|
+
|
|
217
|
+
// Should not receive new messages after close
|
|
218
|
+
expect(receivedMessages.length).toBe(initialCount);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should fire onclose exactly once", async () => {
|
|
222
|
+
const { client } = createBridgeTransportPair();
|
|
223
|
+
|
|
224
|
+
await client.start();
|
|
225
|
+
|
|
226
|
+
let closeCount = 0;
|
|
227
|
+
client.onclose = () => {
|
|
228
|
+
closeCount++;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
await client.close();
|
|
232
|
+
await client.close(); // Try closing again
|
|
233
|
+
|
|
234
|
+
expect(closeCount).toBe(1);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("error handling", () => {
|
|
239
|
+
it("should catch and forward errors from onmessage handler", async () => {
|
|
240
|
+
const { client, server } = createBridgeTransportPair();
|
|
241
|
+
|
|
242
|
+
await server.start();
|
|
243
|
+
await client.start();
|
|
244
|
+
|
|
245
|
+
const errors: Error[] = [];
|
|
246
|
+
server.onerror = (error) => {
|
|
247
|
+
errors.push(error);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
server.onmessage = () => {
|
|
251
|
+
throw new Error("Test error");
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
await client.send({ jsonrpc: "2.0", id: 1, method: "test" });
|
|
255
|
+
|
|
256
|
+
// Wait for microtask to process and error to be forwarded
|
|
257
|
+
await waitFor(() => errors.length >= 1);
|
|
258
|
+
|
|
259
|
+
expect(errors).toHaveLength(1);
|
|
260
|
+
expect(errors[0].message).toBe("Test error");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("should continue processing messages after error", async () => {
|
|
264
|
+
const { client, server } = createBridgeTransportPair();
|
|
265
|
+
|
|
266
|
+
await server.start();
|
|
267
|
+
await client.start();
|
|
268
|
+
|
|
269
|
+
const receivedMessages: JSONRPCMessage[] = [];
|
|
270
|
+
const errors: Error[] = [];
|
|
271
|
+
|
|
272
|
+
let callCount = 0;
|
|
273
|
+
server.onmessage = (msg) => {
|
|
274
|
+
callCount++;
|
|
275
|
+
if (callCount === 1) {
|
|
276
|
+
throw new Error("First message error");
|
|
277
|
+
}
|
|
278
|
+
receivedMessages.push(msg);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
server.onerror = (error) => {
|
|
282
|
+
errors.push(error);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
await client.send({ jsonrpc: "2.0", id: 1, method: "test1" });
|
|
286
|
+
await client.send({ jsonrpc: "2.0", id: 2, method: "test2" });
|
|
287
|
+
|
|
288
|
+
// Wait for microtask to process
|
|
289
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
290
|
+
|
|
291
|
+
expect(errors).toHaveLength(1);
|
|
292
|
+
expect(receivedMessages).toHaveLength(1);
|
|
293
|
+
expect(receivedMessages[0].id).toBe(2);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe("integration with MCP SDK", () => {
|
|
298
|
+
it("should work with MCP Client and Server", async () => {
|
|
299
|
+
const { client: clientTransport, server: serverTransport } =
|
|
300
|
+
createBridgeTransportPair();
|
|
301
|
+
|
|
302
|
+
const client = new Client({ name: "test-client", version: "1.0.0" });
|
|
303
|
+
const server = new Server({ name: "test-server", version: "1.0.0" });
|
|
304
|
+
|
|
305
|
+
// Connect server first
|
|
306
|
+
await server.connect(serverTransport);
|
|
307
|
+
|
|
308
|
+
// Connect client
|
|
309
|
+
await client.connect(clientTransport);
|
|
310
|
+
|
|
311
|
+
// Verify connection is established
|
|
312
|
+
expect(clientTransport.started).toBe(true);
|
|
313
|
+
expect(serverTransport.started).toBe(true);
|
|
314
|
+
|
|
315
|
+
// Clean up
|
|
316
|
+
await client.close();
|
|
317
|
+
await server.close();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("should handle initialize handshake", async () => {
|
|
321
|
+
const { client: clientTransport, server: serverTransport } =
|
|
322
|
+
createBridgeTransportPair();
|
|
323
|
+
|
|
324
|
+
const client = new Client({ name: "test-client", version: "1.0.0" });
|
|
325
|
+
const server = new Server({ name: "test-server", version: "1.0.0" });
|
|
326
|
+
|
|
327
|
+
await server.connect(serverTransport);
|
|
328
|
+
await client.connect(clientTransport);
|
|
329
|
+
|
|
330
|
+
// Wait for initialization
|
|
331
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
332
|
+
|
|
333
|
+
// Verify connection is established (client should have received initialize response)
|
|
334
|
+
expect(clientTransport.started).toBe(true);
|
|
335
|
+
expect(serverTransport.started).toBe(true);
|
|
336
|
+
|
|
337
|
+
await client.close();
|
|
338
|
+
await server.close();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("send() before start()", () => {
|
|
343
|
+
it("should allow sending messages before start (messages may be queued)", async () => {
|
|
344
|
+
const { client, server } = createBridgeTransportPair();
|
|
345
|
+
|
|
346
|
+
// Send message before starting - should not throw
|
|
347
|
+
await expect(
|
|
348
|
+
client.send({ jsonrpc: "2.0", id: 1, method: "test1" }),
|
|
349
|
+
).resolves.toBeUndefined();
|
|
350
|
+
|
|
351
|
+
// Set handler and start
|
|
352
|
+
const receivedMessages: JSONRPCMessage[] = [];
|
|
353
|
+
server.onmessage = (msg) => receivedMessages.push(msg);
|
|
354
|
+
await server.start();
|
|
355
|
+
await client.start();
|
|
356
|
+
|
|
357
|
+
// Send another message after start to verify normal operation
|
|
358
|
+
await client.send({ jsonrpc: "2.0", id: 2, method: "test2" });
|
|
359
|
+
|
|
360
|
+
// Wait for delivery
|
|
361
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
362
|
+
|
|
363
|
+
// At least the message sent after start should be delivered
|
|
364
|
+
// (messages sent before start may or may not be delivered depending on timing)
|
|
365
|
+
expect(receivedMessages.length).toBeGreaterThanOrEqual(1);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|