@contextual-io/cli 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -1
- package/dist/base.js +7 -1
- package/dist/commands/mcp/index.d.ts +11 -0
- package/dist/commands/mcp/index.js +25 -0
- package/dist/commands/mcp/serve.d.ts +17 -0
- package/dist/commands/mcp/serve.js +124 -0
- package/dist/mcp/bridge-manager.d.ts +29 -0
- package/dist/mcp/bridge-manager.js +260 -0
- package/dist/mcp/contracts.d.ts +43 -0
- package/dist/mcp/contracts.js +27 -0
- package/dist/mcp/http-server.d.ts +45 -0
- package/dist/mcp/http-server.js +242 -0
- package/dist/mcp/logger.d.ts +4 -0
- package/dist/mcp/logger.js +9 -0
- package/dist/mcp/runtime.d.ts +24 -0
- package/dist/mcp/runtime.js +297 -0
- package/dist/mcp/server.d.ts +90 -0
- package/dist/mcp/server.js +308 -0
- package/dist/mcp/session.d.ts +42 -0
- package/dist/mcp/session.js +75 -0
- package/dist/mcp/socket-bridge.d.ts +67 -0
- package/dist/mcp/socket-bridge.js +357 -0
- package/dist/models/mcp.d.ts +288 -0
- package/dist/models/mcp.js +137 -0
- package/dist/utils/endpoints.d.ts +1 -0
- package/dist/utils/endpoints.js +1 -0
- package/oclif.manifest.json +116 -1
- package/package.json +5 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { isInArray, makePrefixed } from "./contracts.js";
|
|
4
|
+
const defaultMaxResponseBytes = 512 * 1024;
|
|
5
|
+
const defaultToolPrefix = "ctxl_";
|
|
6
|
+
const baseToolNames = ["list_sessions", "info"];
|
|
7
|
+
const emptyInputSchema = {
|
|
8
|
+
additionalProperties: false,
|
|
9
|
+
properties: {},
|
|
10
|
+
type: "object",
|
|
11
|
+
};
|
|
12
|
+
export { defaultToolPrefix };
|
|
13
|
+
const toToolErrorResult = (message) => ({
|
|
14
|
+
content: [
|
|
15
|
+
{
|
|
16
|
+
text: message,
|
|
17
|
+
type: "text",
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
isError: true,
|
|
21
|
+
});
|
|
22
|
+
const truncatePayload = (payload, maxBytes) => {
|
|
23
|
+
const serialized = JSON.stringify(payload);
|
|
24
|
+
const bytes = Buffer.byteLength(serialized, "utf8");
|
|
25
|
+
if (bytes <= maxBytes) {
|
|
26
|
+
return payload;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
bytes,
|
|
30
|
+
preview: serialized.slice(0, 2000),
|
|
31
|
+
truncated: true,
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
const isRecord = (value) => (value !== null && typeof value === "object" && !Array.isArray(value));
|
|
35
|
+
export const toExposedDynamicToolName = (toolName, prefix) => prefix && !toolName.startsWith(prefix) ? `${prefix}${toolName}` : toolName;
|
|
36
|
+
const mapDynamicToolForExposure = (tool, prefix) => ({
|
|
37
|
+
...tool,
|
|
38
|
+
name: toExposedDynamicToolName(tool.name, prefix),
|
|
39
|
+
});
|
|
40
|
+
const toToolSuccessResult = (payload, maxResponseBytes) => {
|
|
41
|
+
const bounded = truncatePayload(payload, maxResponseBytes);
|
|
42
|
+
const text = JSON.stringify(bounded);
|
|
43
|
+
const structuredContent = isRecord(bounded)
|
|
44
|
+
? bounded
|
|
45
|
+
: undefined;
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
text,
|
|
50
|
+
type: "text",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
...(structuredContent && { structuredContent }),
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const injectDisambiguator = (inputSchema, disambiguator) => {
|
|
57
|
+
const properties = inputSchema.properties ?? {};
|
|
58
|
+
if (disambiguator.key in properties) {
|
|
59
|
+
return inputSchema;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...inputSchema,
|
|
63
|
+
properties: {
|
|
64
|
+
[disambiguator.key]: {
|
|
65
|
+
description: disambiguator.description,
|
|
66
|
+
type: "string",
|
|
67
|
+
},
|
|
68
|
+
...properties,
|
|
69
|
+
},
|
|
70
|
+
required: [disambiguator.key, ...(inputSchema.required ?? [])],
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const isNullableAnyOf = (prop) => {
|
|
74
|
+
const variants = prop.anyOf ?? prop.oneOf;
|
|
75
|
+
if (!Array.isArray(variants) || variants.length !== 2)
|
|
76
|
+
return undefined;
|
|
77
|
+
const nullVariant = variants.find((v) => v.type === "null");
|
|
78
|
+
const innerVariant = variants.find((v) => v.type !== "null");
|
|
79
|
+
if (!nullVariant || !innerVariant)
|
|
80
|
+
return undefined;
|
|
81
|
+
return { inner: innerVariant };
|
|
82
|
+
};
|
|
83
|
+
const flattenNullableProperty = (prop) => {
|
|
84
|
+
const nullable = isNullableAnyOf(prop);
|
|
85
|
+
if (!nullable)
|
|
86
|
+
return prop;
|
|
87
|
+
const { inner } = nullable;
|
|
88
|
+
const description = (prop.description ?? inner.description);
|
|
89
|
+
if (inner.enum && Array.isArray(inner.enum)) {
|
|
90
|
+
const innerType = inner.type;
|
|
91
|
+
const nullableType = innerType
|
|
92
|
+
? (Array.isArray(innerType) ? [...innerType, "null"] : [innerType, "null"])
|
|
93
|
+
: undefined;
|
|
94
|
+
return {
|
|
95
|
+
...inner,
|
|
96
|
+
enum: [...inner.enum, null],
|
|
97
|
+
...(nullableType && { type: nullableType }),
|
|
98
|
+
...(description && { description }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const innerType = inner.type;
|
|
102
|
+
const types = Array.isArray(innerType) ? [...innerType, "null"] : [innerType, "null"];
|
|
103
|
+
return {
|
|
104
|
+
...inner,
|
|
105
|
+
type: types,
|
|
106
|
+
...(description && { description }),
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
const normalizeSchemaForMcp = (inputSchema) => {
|
|
110
|
+
const properties = inputSchema.properties ?? {};
|
|
111
|
+
const normalizedProperties = {};
|
|
112
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
113
|
+
normalizedProperties[key] = flattenNullableProperty(prop);
|
|
114
|
+
}
|
|
115
|
+
const allKeys = Object.keys(normalizedProperties);
|
|
116
|
+
return {
|
|
117
|
+
...inputSchema,
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
properties: normalizedProperties,
|
|
120
|
+
required: allKeys,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
const resolvePropertyType = (prop) => {
|
|
124
|
+
if (typeof prop.type === "string")
|
|
125
|
+
return prop.type;
|
|
126
|
+
if (Array.isArray(prop.type))
|
|
127
|
+
return prop.type.find((t) => t !== "null");
|
|
128
|
+
const nullable = isNullableAnyOf(prop);
|
|
129
|
+
if (nullable) {
|
|
130
|
+
const innerType = nullable.inner.type;
|
|
131
|
+
return typeof innerType === "string" ? innerType : undefined;
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
};
|
|
135
|
+
const isNullableProperty = (prop) => {
|
|
136
|
+
if (isNullableAnyOf(prop))
|
|
137
|
+
return true;
|
|
138
|
+
const { type } = prop;
|
|
139
|
+
if (Array.isArray(type) && type.includes("null"))
|
|
140
|
+
return true;
|
|
141
|
+
return false;
|
|
142
|
+
};
|
|
143
|
+
const coerceArgs = (args, schema) => {
|
|
144
|
+
const properties = schema.properties ?? {};
|
|
145
|
+
const coerced = { ...args };
|
|
146
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
147
|
+
const schemaProp = prop;
|
|
148
|
+
const value = coerced[key];
|
|
149
|
+
const targetType = resolvePropertyType(schemaProp);
|
|
150
|
+
const nullable = isNullableProperty(schemaProp);
|
|
151
|
+
if (value === undefined || value === null) {
|
|
152
|
+
coerced[key] = nullable ? null : value;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if ((targetType === "number" || targetType === "integer") && typeof value === "string") {
|
|
156
|
+
const parsed = Number(value);
|
|
157
|
+
if (!Number.isNaN(parsed)) {
|
|
158
|
+
coerced[key] = targetType === "integer" ? Math.trunc(parsed) : parsed;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (targetType === "boolean" && typeof value === "string") {
|
|
162
|
+
if (value === "true") {
|
|
163
|
+
coerced[key] = true;
|
|
164
|
+
}
|
|
165
|
+
else if (value === "false") {
|
|
166
|
+
coerced[key] = false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return coerced;
|
|
171
|
+
};
|
|
172
|
+
const getCatalogKey = (toolNames, dynamicTools, prefix) => {
|
|
173
|
+
const staticKey = [...toolNames].sort().join("|");
|
|
174
|
+
const dynamicKey = [...dynamicTools]
|
|
175
|
+
.map(tool => `${toExposedDynamicToolName(tool.name, prefix)}:${tool.description}`)
|
|
176
|
+
.sort()
|
|
177
|
+
.join("|");
|
|
178
|
+
return `${staticKey}::${dynamicKey}`;
|
|
179
|
+
};
|
|
180
|
+
const asRecord = (value) => isRecord(value)
|
|
181
|
+
? value
|
|
182
|
+
: {};
|
|
183
|
+
export const createCtxlMcpServer = ({ callDynamicTool, disambiguator, globalDisambiguatorValue, handlers, isBound, listDynamicTools, maxResponseBytes = defaultMaxResponseBytes, onToolCall, toolPrefix = "", version, visibleTools, }) => {
|
|
184
|
+
const injectDisambiguatorIntoSchemas = disambiguator && !globalDisambiguatorValue;
|
|
185
|
+
const prefixed = makePrefixed(toolPrefix);
|
|
186
|
+
const ctxlToolNames = baseToolNames.map(name => prefixed(name));
|
|
187
|
+
const mcpServer = new McpServer({
|
|
188
|
+
name: "ctxl-mcp",
|
|
189
|
+
version,
|
|
190
|
+
}, {
|
|
191
|
+
capabilities: {
|
|
192
|
+
tools: {
|
|
193
|
+
listChanged: true,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
const toolDescriptors = {
|
|
198
|
+
[prefixed("info")]: {
|
|
199
|
+
description: "Return runtime state: tenant, interface type, connected flow details, and any recent errors.",
|
|
200
|
+
inputSchema: emptyInputSchema,
|
|
201
|
+
},
|
|
202
|
+
[prefixed("list_sessions")]: {
|
|
203
|
+
description: disambiguator
|
|
204
|
+
? "List flows with available browser sessions. Returns flow IDs and names. Use only if the flow ID is unknown. Call a tool for a flow to initiate a session."
|
|
205
|
+
: "List flows with available browser sessions that can be connected to. Only shows flows where a browser is not already connected to another MCP client. If the desired flow is not listed, the user needs to open a new browser session for that flow.",
|
|
206
|
+
inputSchema: {
|
|
207
|
+
additionalProperties: false,
|
|
208
|
+
properties: {
|
|
209
|
+
flowId: { description: "Filter to a specific flow by ID.", type: "string" },
|
|
210
|
+
},
|
|
211
|
+
type: "object",
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
const listTools = async () => {
|
|
216
|
+
const dynamicTools = listDynamicTools()
|
|
217
|
+
.map(tool => mapDynamicToolForExposure(tool, toolPrefix))
|
|
218
|
+
.filter(tool => !isInArray(ctxlToolNames, tool.name))
|
|
219
|
+
.map(({ annotations, description, inputSchema, name, outputSchema, title }) => {
|
|
220
|
+
let schema = inputSchema;
|
|
221
|
+
if (injectDisambiguatorIntoSchemas) {
|
|
222
|
+
schema = injectDisambiguator(schema, disambiguator);
|
|
223
|
+
}
|
|
224
|
+
schema = normalizeSchemaForMcp(schema);
|
|
225
|
+
return {
|
|
226
|
+
...(annotations && { annotations }),
|
|
227
|
+
description,
|
|
228
|
+
inputSchema: schema,
|
|
229
|
+
name,
|
|
230
|
+
...(outputSchema && { outputSchema }),
|
|
231
|
+
...(title && { title }),
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
return {
|
|
235
|
+
tools: [
|
|
236
|
+
...ctxlToolNames.map((name) => {
|
|
237
|
+
const { description, inputSchema } = toolDescriptors[name];
|
|
238
|
+
return { description, inputSchema, name };
|
|
239
|
+
}),
|
|
240
|
+
...dynamicTools,
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
const callTool = async (toolName, args) => {
|
|
245
|
+
const dynamicTool = listDynamicTools().find(tool => toExposedDynamicToolName(tool.name, toolPrefix) === toolName);
|
|
246
|
+
try {
|
|
247
|
+
if (isInArray(ctxlToolNames, toolName)) {
|
|
248
|
+
onToolCall?.(toolName);
|
|
249
|
+
switch (toolName) {
|
|
250
|
+
case prefixed("info"): {
|
|
251
|
+
return toToolSuccessResult(await handlers.info(), maxResponseBytes);
|
|
252
|
+
}
|
|
253
|
+
case prefixed("list_sessions"): {
|
|
254
|
+
return toToolSuccessResult(await handlers.listSessions({
|
|
255
|
+
flowId: args.flowId === undefined ? undefined : `${args.flowId}`,
|
|
256
|
+
}), maxResponseBytes);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (!dynamicTool) {
|
|
261
|
+
return toToolErrorResult(`Unknown tool '${toolName}'`);
|
|
262
|
+
}
|
|
263
|
+
const originalSchema = dynamicTool.inputSchema;
|
|
264
|
+
const coercedArgs = coerceArgs(args, originalSchema);
|
|
265
|
+
if (disambiguator) {
|
|
266
|
+
const { key } = disambiguator;
|
|
267
|
+
const resolvedValue = globalDisambiguatorValue
|
|
268
|
+
?? (typeof coercedArgs[key] === "string" ? coercedArgs[key] : undefined);
|
|
269
|
+
if (!resolvedValue) {
|
|
270
|
+
return toToolErrorResult(`Missing required parameter '${key}'. The user should know which flow they are working in. Use list_sessions if the flow ID is unknown.`);
|
|
271
|
+
}
|
|
272
|
+
const toolOwnsKey = !injectDisambiguatorIntoSchemas
|
|
273
|
+
|| (key in (originalSchema.properties ?? {}));
|
|
274
|
+
const { [key]: _, ...rest } = coercedArgs;
|
|
275
|
+
const forwardArgs = toolOwnsKey ? coercedArgs : rest;
|
|
276
|
+
const dynamicPayload = await callDynamicTool(dynamicTool.name, forwardArgs, resolvedValue);
|
|
277
|
+
return toToolSuccessResult(dynamicPayload, maxResponseBytes);
|
|
278
|
+
}
|
|
279
|
+
if (!isBound()) {
|
|
280
|
+
return toToolErrorResult("No active session. Call list_sessions to see available flows and pass a flowId to connect.");
|
|
281
|
+
}
|
|
282
|
+
const dynamicPayload = await callDynamicTool(dynamicTool.name, coercedArgs);
|
|
283
|
+
return toToolSuccessResult(dynamicPayload, maxResponseBytes);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
const message = error instanceof Error ? error.message : `${error}`;
|
|
287
|
+
return toToolErrorResult(message);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
mcpServer.server.setRequestHandler(ListToolsRequestSchema, listTools);
|
|
291
|
+
mcpServer.server.setRequestHandler(CallToolRequestSchema, async ({ params: { arguments: rawArgs, name } }) => callTool(name, asRecord(rawArgs)));
|
|
292
|
+
let lastCatalogKey = getCatalogKey(visibleTools(), listDynamicTools(), toolPrefix);
|
|
293
|
+
return {
|
|
294
|
+
debug: {
|
|
295
|
+
callTool,
|
|
296
|
+
listTools,
|
|
297
|
+
},
|
|
298
|
+
async notifyToolsChanged(force = false) {
|
|
299
|
+
const nextCatalogKey = getCatalogKey(visibleTools(), listDynamicTools(), toolPrefix);
|
|
300
|
+
if (!force && nextCatalogKey === lastCatalogKey) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
lastCatalogKey = nextCatalogKey;
|
|
304
|
+
mcpServer.sendToolListChanged();
|
|
305
|
+
},
|
|
306
|
+
server: mcpServer,
|
|
307
|
+
};
|
|
308
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { McpBindFailureCode, McpDetachReason } from "../models/mcp.js";
|
|
2
|
+
import type { McpSocketBridgeErrorCode } from "./socket-bridge.js";
|
|
3
|
+
export type McpServerStateValue = "error" | "initializing" | "ready";
|
|
4
|
+
export type McpBindingStateValue = "binding" | "bound" | "unbound";
|
|
5
|
+
export type McpLastError = {
|
|
6
|
+
at: number;
|
|
7
|
+
code: McpSessionErrorCode;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
export type McpSessionErrorCode = "CONNECT_FAILED" | "MANIFEST_FAILED" | "SOCKET_DISCONNECTED" | McpBindFailureCode | McpDetachReason | McpSocketBridgeErrorCode;
|
|
11
|
+
export type McpBoundConnection = {
|
|
12
|
+
boundAt: number;
|
|
13
|
+
flowId: string;
|
|
14
|
+
flowName: string;
|
|
15
|
+
};
|
|
16
|
+
export type McpSessionSnapshot = {
|
|
17
|
+
bindingState: McpBindingStateValue;
|
|
18
|
+
connection?: McpBoundConnection;
|
|
19
|
+
lastError?: McpLastError;
|
|
20
|
+
serverState: McpServerStateValue;
|
|
21
|
+
};
|
|
22
|
+
type ServerStateListener = (next: McpServerStateValue, previous: McpServerStateValue) => void;
|
|
23
|
+
type BindingStateListener = (next: McpBindingStateValue, previous: McpBindingStateValue) => void;
|
|
24
|
+
export declare class McpSessionState {
|
|
25
|
+
private bindingListeners;
|
|
26
|
+
private bindingState;
|
|
27
|
+
private connection?;
|
|
28
|
+
private lastError?;
|
|
29
|
+
private serverListeners;
|
|
30
|
+
private serverState;
|
|
31
|
+
beginBind(): void;
|
|
32
|
+
getSnapshot(): McpSessionSnapshot;
|
|
33
|
+
markBound(connection: McpBoundConnection): void;
|
|
34
|
+
markError(code: McpSessionErrorCode, message: string): void;
|
|
35
|
+
markReady(): void;
|
|
36
|
+
markUnbound(): void;
|
|
37
|
+
onBindingStateChange(listener: BindingStateListener): () => void;
|
|
38
|
+
onServerStateChange(listener: ServerStateListener): () => void;
|
|
39
|
+
private setBindingState;
|
|
40
|
+
private setServerState;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export class McpSessionState {
|
|
2
|
+
bindingListeners = new Set();
|
|
3
|
+
bindingState = "unbound";
|
|
4
|
+
connection;
|
|
5
|
+
lastError;
|
|
6
|
+
serverListeners = new Set();
|
|
7
|
+
serverState = "initializing";
|
|
8
|
+
beginBind() {
|
|
9
|
+
if (this.serverState !== "ready") {
|
|
10
|
+
throw new Error("Cannot bind: server is not ready");
|
|
11
|
+
}
|
|
12
|
+
this.setBindingState("binding");
|
|
13
|
+
}
|
|
14
|
+
getSnapshot() {
|
|
15
|
+
return {
|
|
16
|
+
bindingState: this.bindingState,
|
|
17
|
+
serverState: this.serverState,
|
|
18
|
+
...(this.connection && { connection: this.connection }),
|
|
19
|
+
...(this.lastError && { lastError: this.lastError }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
markBound(connection) {
|
|
23
|
+
this.connection = connection;
|
|
24
|
+
this.setBindingState("bound");
|
|
25
|
+
}
|
|
26
|
+
markError(code, message) {
|
|
27
|
+
this.lastError = {
|
|
28
|
+
at: Date.now(),
|
|
29
|
+
code,
|
|
30
|
+
message,
|
|
31
|
+
};
|
|
32
|
+
this.connection = undefined;
|
|
33
|
+
this.setBindingState("unbound");
|
|
34
|
+
this.setServerState("error");
|
|
35
|
+
}
|
|
36
|
+
markReady() {
|
|
37
|
+
this.setServerState("ready");
|
|
38
|
+
}
|
|
39
|
+
markUnbound() {
|
|
40
|
+
this.connection = undefined;
|
|
41
|
+
this.setBindingState("unbound");
|
|
42
|
+
}
|
|
43
|
+
onBindingStateChange(listener) {
|
|
44
|
+
this.bindingListeners.add(listener);
|
|
45
|
+
return () => {
|
|
46
|
+
this.bindingListeners.delete(listener);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
onServerStateChange(listener) {
|
|
50
|
+
this.serverListeners.add(listener);
|
|
51
|
+
return () => {
|
|
52
|
+
this.serverListeners.delete(listener);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
setBindingState(next) {
|
|
56
|
+
const previous = this.bindingState;
|
|
57
|
+
if (previous === next) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.bindingState = next;
|
|
61
|
+
for (const listener of this.bindingListeners) {
|
|
62
|
+
listener(next, previous);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
setServerState(next) {
|
|
66
|
+
const previous = this.serverState;
|
|
67
|
+
if (previous === next) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.serverState = next;
|
|
71
|
+
for (const listener of this.serverListeners) {
|
|
72
|
+
listener(next, previous);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { McpBindSessionResponse, McpCommandStatus, McpListSessionsResponse, McpManifestResponse, McpToolRegistrySync } from "../models/mcp.js";
|
|
2
|
+
import { McpDetachedPayload, McpSessionAvailablePayload, McpSessionUnavailablePayload } from "./contracts.js";
|
|
3
|
+
export type McpSocketBridgeErrorCode = "ALREADY_CONNECTED" | "BIND_FAILED" | "BIND_TIMEOUT" | "CONNECT_ERROR" | "DISCONNECTED_DURING_COMMAND" | "INVALID_CONNECT_ROUTE" | "LIST_SESSIONS_FAILED" | "MANIFEST_FETCH_FAILED" | "MANIFEST_TIMEOUT" | "MCP_ERROR" | "NOT_CONNECTED";
|
|
4
|
+
export type McpSocketRoute = {
|
|
5
|
+
connectUrl: string;
|
|
6
|
+
namespace: "/mcp";
|
|
7
|
+
namespaceUrl: string;
|
|
8
|
+
path: string;
|
|
9
|
+
url: string;
|
|
10
|
+
};
|
|
11
|
+
type ConnectParams = {
|
|
12
|
+
interfaceType: string;
|
|
13
|
+
orgId: string;
|
|
14
|
+
token: string;
|
|
15
|
+
url: string;
|
|
16
|
+
};
|
|
17
|
+
type ConnectResult = {
|
|
18
|
+
connectedAt: number;
|
|
19
|
+
};
|
|
20
|
+
export declare const getMcpSocketRoute: ({ url }: Pick<ConnectParams, "url">) => McpSocketRoute;
|
|
21
|
+
export type McpSocketBridgeHandlers = {
|
|
22
|
+
onCommandStatus?: (payload: McpCommandStatus) => void;
|
|
23
|
+
onDetached?: (payload: McpDetachedPayload) => void;
|
|
24
|
+
onDisconnected?: (reason: string) => void;
|
|
25
|
+
onSessionAvailable?: (payload: McpSessionAvailablePayload) => void;
|
|
26
|
+
onSessionUnavailable?: (payload: McpSessionUnavailablePayload) => void;
|
|
27
|
+
onToolRegistrySync?: (payload: McpToolRegistrySync) => void;
|
|
28
|
+
};
|
|
29
|
+
type RunToolParams = {
|
|
30
|
+
args: Record<string, unknown>;
|
|
31
|
+
name: string;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
};
|
|
34
|
+
export declare class McpSocketBridgeError extends Error {
|
|
35
|
+
readonly code: McpSocketBridgeErrorCode;
|
|
36
|
+
constructor(message: string, code: McpSocketBridgeErrorCode);
|
|
37
|
+
}
|
|
38
|
+
export declare class McpSocketBridge {
|
|
39
|
+
private boundFlowId?;
|
|
40
|
+
private commandQueue;
|
|
41
|
+
private handlers;
|
|
42
|
+
private pendingCommands;
|
|
43
|
+
private socket?;
|
|
44
|
+
constructor(handlers?: McpSocketBridgeHandlers);
|
|
45
|
+
get isConnected(): boolean;
|
|
46
|
+
bindSession({ flowId }: {
|
|
47
|
+
flowId: string;
|
|
48
|
+
}): Promise<McpBindSessionResponse>;
|
|
49
|
+
connect({ interfaceType, orgId, token, url }: ConnectParams): Promise<ConnectResult>;
|
|
50
|
+
disconnect(): Promise<{
|
|
51
|
+
detachedAt: number;
|
|
52
|
+
} | undefined>;
|
|
53
|
+
hasActiveSocket(): boolean;
|
|
54
|
+
listSessions({ flowId, interfaceType }: {
|
|
55
|
+
flowId?: string;
|
|
56
|
+
interfaceType: string;
|
|
57
|
+
}): Promise<McpListSessionsResponse>;
|
|
58
|
+
requestManifest(interfaceType: string): Promise<McpManifestResponse>;
|
|
59
|
+
runTool(params: RunToolParams): Promise<unknown>;
|
|
60
|
+
unbindSession(): Promise<void>;
|
|
61
|
+
private bindPersistentListeners;
|
|
62
|
+
private executeToolCommand;
|
|
63
|
+
private notifyRemoteDisconnect;
|
|
64
|
+
private rejectAllPendingCommands;
|
|
65
|
+
private requireSocket;
|
|
66
|
+
}
|
|
67
|
+
export {};
|