@dsh-cc/mcp-client 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.i18n.yaml +6 -0
- package/README.md +149 -0
- package/README.zh.md +150 -0
- package/lib/auth.d.ts +66 -0
- package/lib/auth.d.ts.map +1 -0
- package/lib/auth.js +121 -0
- package/lib/auth.js.map +1 -0
- package/lib/connection.d.ts +98 -0
- package/lib/connection.d.ts.map +1 -0
- package/lib/connection.js +409 -0
- package/lib/connection.js.map +1 -0
- package/lib/defer.d.ts +39 -0
- package/lib/defer.d.ts.map +1 -0
- package/lib/defer.js +40 -0
- package/lib/defer.js.map +1 -0
- package/lib/index.d.ts +129 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +198 -0
- package/lib/index.js.map +1 -0
- package/lib/invariant.d.ts +16 -0
- package/lib/invariant.d.ts.map +1 -0
- package/lib/invariant.js +22 -0
- package/lib/invariant.js.map +1 -0
- package/lib/prompts.d.ts +44 -0
- package/lib/prompts.d.ts.map +1 -0
- package/lib/prompts.js +166 -0
- package/lib/prompts.js.map +1 -0
- package/lib/registry.d.ts +94 -0
- package/lib/registry.d.ts.map +1 -0
- package/lib/registry.js +101 -0
- package/lib/registry.js.map +1 -0
- package/lib/resources.d.ts +42 -0
- package/lib/resources.d.ts.map +1 -0
- package/lib/resources.js +136 -0
- package/lib/resources.js.map +1 -0
- package/lib/stdio-stderr.d.ts +66 -0
- package/lib/stdio-stderr.d.ts.map +1 -0
- package/lib/stdio-stderr.js +187 -0
- package/lib/stdio-stderr.js.map +1 -0
- package/lib/tools.d.ts +161 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +373 -0
- package/lib/tools.js.map +1 -0
- package/lib/transport.d.ts +50 -0
- package/lib/transport.d.ts.map +1 -0
- package/lib/transport.js +79 -0
- package/lib/transport.js.map +1 -0
- package/package.json +62 -0
package/lib/prompts.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt bridge: exposes an MCP server's `prompts` capability to the model as
|
|
3
|
+
* discoverable skills on the `ctx.skills` registry, one runtime skill per MCP
|
|
4
|
+
* prompt, when the server declares the capability.
|
|
5
|
+
*
|
|
6
|
+
* The harness's skill registry enforces a lowercase-kebab name grammar
|
|
7
|
+
* (`[a-z0-9]+(?:-[a-z0-9]+)*`), which `mcp__<server>__<prompt>` (with its
|
|
8
|
+
* double underscores) cannot satisfy. Names are therefore mapped to kebab-case
|
|
9
|
+
* (`mcp-<server>-<prompt>`); see {@link promptSkillName}. The body is the
|
|
10
|
+
* prompt's rendered text (or, for prompts that require arguments, the argument
|
|
11
|
+
* contract) — plain prose only. MCP-sourced skills must never embed or invoke
|
|
12
|
+
* shell; the body is always inert text.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import { GetPromptRequestSchema, ListPromptsResultSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
17
|
+
import { isSkillName } from '@deepseek-ai/dsh-skill';
|
|
18
|
+
/**
|
|
19
|
+
* Map an MCP prompt identity to a registry-valid skill name. The registry's
|
|
20
|
+
* grammar rejects underscores, so the `mcp__<server>__<prompt>` shape becomes
|
|
21
|
+
* lowercase kebab-case: non-alphanumeric runs collapse to single dashes, and a
|
|
22
|
+
* leading `mcp` prefix is guaranteed. Distinct identities may rarely collapse;
|
|
23
|
+
* the first registration wins and later duplicates log a registry warning.
|
|
24
|
+
*
|
|
25
|
+
* @param serverName - MCP server namespace.
|
|
26
|
+
* @param promptName - the MCP prompt's own name.
|
|
27
|
+
* @returns a valid lowercase-kebab skill name.
|
|
28
|
+
*/
|
|
29
|
+
export function promptSkillName(serverName, promptName) {
|
|
30
|
+
const raw = `mcp-${serverName}-${promptName}`;
|
|
31
|
+
const kebab = raw.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
32
|
+
if (kebab.length === 0)
|
|
33
|
+
return 'mcp';
|
|
34
|
+
return kebab;
|
|
35
|
+
}
|
|
36
|
+
/** Whether an MCP prompt can be fetched without arguments. */
|
|
37
|
+
function argumentless(prompt) {
|
|
38
|
+
return (prompt.arguments ?? []).every(a => a.required !== true);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Render an MCP prompt's messages into plain text. Each message's content block
|
|
42
|
+
* contributes its text; non-text blocks become a short marker. The output is
|
|
43
|
+
* inert prose — never executable code.
|
|
44
|
+
*
|
|
45
|
+
* @param messages - the prompt's message list from `prompts/get`.
|
|
46
|
+
* @returns the joined prompt body.
|
|
47
|
+
*/
|
|
48
|
+
function renderMessages(messages) {
|
|
49
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
50
|
+
const parts = [];
|
|
51
|
+
for (const message of list) {
|
|
52
|
+
const record = message;
|
|
53
|
+
const role = record?.role ?? 'user';
|
|
54
|
+
const content = record?.content;
|
|
55
|
+
parts.push(renderBlock(content, role));
|
|
56
|
+
}
|
|
57
|
+
return parts.filter(p => p.length > 0).join('\n');
|
|
58
|
+
}
|
|
59
|
+
/** Render one content block (array or single) to text. */
|
|
60
|
+
function renderBlock(content, role) {
|
|
61
|
+
const blocks = Array.isArray(content) ? content : [content];
|
|
62
|
+
const text = [];
|
|
63
|
+
for (const block of blocks) {
|
|
64
|
+
if (block === null || typeof block !== 'object') {
|
|
65
|
+
text.push('[unsupported prompt content]');
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const item = block;
|
|
69
|
+
if (item.type === 'text' && item.text !== undefined)
|
|
70
|
+
text.push(item.text);
|
|
71
|
+
else if (item.type === 'text')
|
|
72
|
+
text.push('');
|
|
73
|
+
else if (item.type === 'image')
|
|
74
|
+
text.push('[image content]');
|
|
75
|
+
else
|
|
76
|
+
text.push(`[${item.type ?? 'unsupported'} content]`);
|
|
77
|
+
}
|
|
78
|
+
const joined = text.join('');
|
|
79
|
+
if (joined.length === 0)
|
|
80
|
+
return `[${role} message, no text]`;
|
|
81
|
+
return `${role === 'user' ? 'User' : 'Assistant'}: ${joined}`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Load one MCP prompt body. Argumentless prompts are resolved via `prompts/get`
|
|
85
|
+
* and their rendered text becomes the skill body. Prompts requiring arguments
|
|
86
|
+
* cannot be fetched on demand (there is no prompt-call tool); their body
|
|
87
|
+
* documents the prompt and its argument contract instead, in prose.
|
|
88
|
+
*
|
|
89
|
+
* @param client - connected MCP client.
|
|
90
|
+
* @param prompt - the MCP prompt to load.
|
|
91
|
+
* @returns the skill body text.
|
|
92
|
+
*/
|
|
93
|
+
async function loadPromptBody(client, prompt) {
|
|
94
|
+
if (argumentless(prompt)) {
|
|
95
|
+
try {
|
|
96
|
+
const result = await client.request({ method: 'prompts/get', params: { name: prompt.name } }, GetPromptRequestSchema);
|
|
97
|
+
return renderMessages(result.messages);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Fall through to the static contract below on a get failure.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const args = (prompt.arguments ?? []).map((a) => {
|
|
104
|
+
const required = a.required === true ? ' (required)' : '';
|
|
105
|
+
return `- ${a.name}${required}${a.description !== undefined ? ': ' + a.description : ''}`;
|
|
106
|
+
});
|
|
107
|
+
const flag = (prompt.arguments ?? []).length > 0
|
|
108
|
+
? `\n\nThis MCP prompt requires the following arguments:\n${args.join('\n')}`
|
|
109
|
+
: '';
|
|
110
|
+
const description = prompt.description !== undefined && prompt.description.length > 0
|
|
111
|
+
? `${prompt.description}\n\n`
|
|
112
|
+
: '';
|
|
113
|
+
return `${description}This is an MCP prompt named "${prompt.name}"${flag}. The model should use it to structure its response.`;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Register one skill per MCP prompt for a server on `ctx.skills`. Runs only
|
|
117
|
+
* when the server declared the `prompts` capability. Registering requires the
|
|
118
|
+
* `ctx.skills` service; when it is absent this is a no-op (the harness offers
|
|
119
|
+
* no prompt surface for that server).
|
|
120
|
+
*
|
|
121
|
+
* @param client - connected MCP client used to load prompt bodies.
|
|
122
|
+
* @param ctx - Cordis context; `ctx.skills` is read optionally.
|
|
123
|
+
* @param serverName - server namespace used to derive skill names.
|
|
124
|
+
* @returns disposers for every registered skill (never rejects).
|
|
125
|
+
*/
|
|
126
|
+
export async function syncPrompts(client, ctx, serverName) {
|
|
127
|
+
const skills = ctx.get('skills');
|
|
128
|
+
const disposers = new Map();
|
|
129
|
+
if (skills === undefined)
|
|
130
|
+
return disposers;
|
|
131
|
+
const prompts = [];
|
|
132
|
+
let cursor;
|
|
133
|
+
try {
|
|
134
|
+
do {
|
|
135
|
+
const page = await client.request({ method: 'prompts/list', ...cursor === undefined ? {} : { params: { cursor } } }, ListPromptsResultSchema);
|
|
136
|
+
prompts.push(...page.prompts);
|
|
137
|
+
cursor = page.nextCursor;
|
|
138
|
+
} while (cursor !== undefined);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
ctx.logger.warn(`mcp-client(${serverName}): prompts/list failed, no prompts registered: ${String(error)}`);
|
|
142
|
+
return disposers;
|
|
143
|
+
}
|
|
144
|
+
for (const prompt of prompts) {
|
|
145
|
+
const name = promptSkillName(serverName, prompt.name);
|
|
146
|
+
if (!isSkillName(name) || disposers.has(name))
|
|
147
|
+
continue;
|
|
148
|
+
const body = await loadPromptBody(client, prompt);
|
|
149
|
+
if (body.length === 0)
|
|
150
|
+
continue;
|
|
151
|
+
try {
|
|
152
|
+
const definition = {
|
|
153
|
+
name,
|
|
154
|
+
description: prompt.description ?? `MCP prompt "${prompt.name}" from server "${serverName}"`,
|
|
155
|
+
source: 'runtime',
|
|
156
|
+
content: body,
|
|
157
|
+
};
|
|
158
|
+
disposers.set(name, skills.register(definition));
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
ctx.logger.warn(`mcp-client(${serverName}): could not register prompt skill "${name}": ${String(error)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return disposers;
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central MCP connection registry for the mcp-client package.
|
|
3
|
+
*
|
|
4
|
+
* Each mcp-client plugin instance bridges one MCP server, and previously
|
|
5
|
+
* nothing central could enumerate them. This module defines an optional
|
|
6
|
+
* `mcpConnections` service that gathers every live instance under one roof so
|
|
7
|
+
* host plugins (slash commands such as `/mcp`) can list servers, their
|
|
8
|
+
* connection state, and drive disconnect/reconnect. The service is provided by
|
|
9
|
+
* mcp-client itself; an instance running standalone provides it lazily, while
|
|
10
|
+
* instances sharing a scope reuse the first-provided one, so the registry stays
|
|
11
|
+
* optional (mcp-client keeps working without a consumer).
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { Service, type Context } from '@deepseek-ai/cordis';
|
|
16
|
+
declare module '@deepseek-ai/cordis' {
|
|
17
|
+
interface Context {
|
|
18
|
+
/** The live MCP connection registry, when an mcp-client instance provided it. */
|
|
19
|
+
mcpConnections: McpConnectionsService;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** The connection lifecycle state of one MCP server instance. */
|
|
23
|
+
export type McpConnectionState = 'connecting' | 'ready' | 'error' | 'disconnected';
|
|
24
|
+
/** A public snapshot of one registered MCP server. */
|
|
25
|
+
export interface McpConnectionEntry {
|
|
26
|
+
/** The mcp-client `serverName` this instance bridges. */
|
|
27
|
+
name: string;
|
|
28
|
+
/** Current connection lifecycle state. */
|
|
29
|
+
state: McpConnectionState;
|
|
30
|
+
/** The last error message, when the state is `error`. */
|
|
31
|
+
error?: string;
|
|
32
|
+
/** The number of tools this server currently exposes, when known. */
|
|
33
|
+
toolCount?: number;
|
|
34
|
+
/** Number of tools registered eagerly (including resource-bridge tools), when known. */
|
|
35
|
+
eagerCount?: number;
|
|
36
|
+
/** Number of tools registered deferred (hidden until ToolSearch activation), when known. */
|
|
37
|
+
deferredCount?: number;
|
|
38
|
+
/** Whether interacting with this server requires OAuth authorization. */
|
|
39
|
+
authRequired?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** Per-instance control surface the owning mcp-client plugin wires to its supervisor. */
|
|
42
|
+
export interface McpConnectionControl {
|
|
43
|
+
/** Stop the current connection and unregister its tools; the entry is marked `disconnected`. */
|
|
44
|
+
disconnect(): Promise<void>;
|
|
45
|
+
/** Tear down the current connection and establish a fresh one; the entry is marked `connecting` then `ready`/`error`. */
|
|
46
|
+
reconnect(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** The `mcpConnections` service: enumerate and drive the registered MCP servers. */
|
|
49
|
+
export declare class McpConnectionsService extends Service {
|
|
50
|
+
/** Live instances keyed by `serverName`. */
|
|
51
|
+
private readonly managed;
|
|
52
|
+
constructor(ctx: Context);
|
|
53
|
+
/**
|
|
54
|
+
* Register a live mcp-client instance's control surface, initially `connecting`.
|
|
55
|
+
* @param name - the instance `serverName`.
|
|
56
|
+
* @param control - the disconnect/reconnect control the instance wires.
|
|
57
|
+
* @throws when `name` is already registered (a live duplicate is a config error).
|
|
58
|
+
*/
|
|
59
|
+
register(name: string, control: McpConnectionControl, authRequired?: boolean): void;
|
|
60
|
+
/** Remove an instance (on teardown / full disconnect). */
|
|
61
|
+
unregister(name: string): void;
|
|
62
|
+
/** Report a lifecycle transition for a registered instance. */
|
|
63
|
+
report(name: string, state: McpConnectionState, info?: {
|
|
64
|
+
error?: string;
|
|
65
|
+
}): void;
|
|
66
|
+
/** Record the current tool count for a registered instance. */
|
|
67
|
+
setToolCount(name: string, toolCount: number): void;
|
|
68
|
+
/**
|
|
69
|
+
* Record the eager/deferred tool breakdown for a registered instance.
|
|
70
|
+
* Also overwrites `toolCount` with `eager + deferred`, keeping the
|
|
71
|
+
* invariant `eagerCount + deferredCount === toolCount`.
|
|
72
|
+
*/
|
|
73
|
+
setToolBreakdown(name: string, breakdown: {
|
|
74
|
+
eager: number;
|
|
75
|
+
deferred: number;
|
|
76
|
+
}): void;
|
|
77
|
+
/** A snapshot of every registered server today. */
|
|
78
|
+
entries(): McpConnectionEntry[];
|
|
79
|
+
/**
|
|
80
|
+
* Disconnect a registered server: stop its connection and unregister its
|
|
81
|
+
* tools, leaving the entry marked `disconnected`.
|
|
82
|
+
* @param name - the instance `serverName`.
|
|
83
|
+
* @throws when no such server is registered or its control rejects.
|
|
84
|
+
*/
|
|
85
|
+
disconnect(name: string): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Reconnect a registered server: tear down and establish a fresh connection.
|
|
88
|
+
* @param name - the instance `serverName`.
|
|
89
|
+
* @throws when no such server is registered or its control rejects.
|
|
90
|
+
*/
|
|
91
|
+
reconnect(name: string): Promise<void>;
|
|
92
|
+
private require;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAE3D,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,iFAAiF;QACjF,cAAc,EAAE,qBAAqB,CAAA;KACtC;CACF;AAED,iEAAiE;AACjE,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,OAAO,GAAG,OAAO,GAAG,cAAc,CAAA;AAElF,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAA;IACZ,0CAA0C;IAC1C,KAAK,EAAE,kBAAkB,CAAA;IACzB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,4FAA4F;IAC5F,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,yEAAyE;IACzE,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,yFAAyF;AACzF,MAAM,WAAW,oBAAoB;IACnC,gGAAgG;IAChG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3B,yHAAyH;IACzH,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAQD,oFAAoF;AACpF,qBAAa,qBAAsB,SAAQ,OAAO;IAChD,4CAA4C;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;gBAEzC,GAAG,EAAE,OAAO;IAIxB;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,EAAE,YAAY,CAAC,EAAE,OAAO,GAAG,IAAI;IAOnF,0DAA0D;IAC1D,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI9B,+DAA+D;IAC/D,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,EAAE,IAAI,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,IAAI;IAQpF,+DAA+D;IAC/D,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAKnD;;;;OAIG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAQpF,mDAAmD;IACnD,OAAO,IAAI,kBAAkB,EAAE;IAI/B;;;;;OAKG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM7C;;;;OAIG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5C,OAAO,CAAC,OAAO;CAKhB"}
|
package/lib/registry.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central MCP connection registry for the mcp-client package.
|
|
3
|
+
*
|
|
4
|
+
* Each mcp-client plugin instance bridges one MCP server, and previously
|
|
5
|
+
* nothing central could enumerate them. This module defines an optional
|
|
6
|
+
* `mcpConnections` service that gathers every live instance under one roof so
|
|
7
|
+
* host plugins (slash commands such as `/mcp`) can list servers, their
|
|
8
|
+
* connection state, and drive disconnect/reconnect. The service is provided by
|
|
9
|
+
* mcp-client itself; an instance running standalone provides it lazily, while
|
|
10
|
+
* instances sharing a scope reuse the first-provided one, so the registry stays
|
|
11
|
+
* optional (mcp-client keeps working without a consumer).
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
16
|
+
/** The `mcpConnections` service: enumerate and drive the registered MCP servers. */
|
|
17
|
+
export class McpConnectionsService extends Service {
|
|
18
|
+
/** Live instances keyed by `serverName`. */
|
|
19
|
+
managed = new Map();
|
|
20
|
+
constructor(ctx) {
|
|
21
|
+
super(ctx, 'mcpConnections');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Register a live mcp-client instance's control surface, initially `connecting`.
|
|
25
|
+
* @param name - the instance `serverName`.
|
|
26
|
+
* @param control - the disconnect/reconnect control the instance wires.
|
|
27
|
+
* @throws when `name` is already registered (a live duplicate is a config error).
|
|
28
|
+
*/
|
|
29
|
+
register(name, control, authRequired) {
|
|
30
|
+
if (this.managed.has(name)) {
|
|
31
|
+
throw new Error(`mcpConnections: server "${name}" is already registered by another mcp-client instance`);
|
|
32
|
+
}
|
|
33
|
+
this.managed.set(name, { entry: { name, state: 'connecting', ...authRequired === undefined ? {} : { authRequired } }, control });
|
|
34
|
+
}
|
|
35
|
+
/** Remove an instance (on teardown / full disconnect). */
|
|
36
|
+
unregister(name) {
|
|
37
|
+
this.managed.delete(name);
|
|
38
|
+
}
|
|
39
|
+
/** Report a lifecycle transition for a registered instance. */
|
|
40
|
+
report(name, state, info = {}) {
|
|
41
|
+
const managed = this.managed.get(name);
|
|
42
|
+
if (!managed)
|
|
43
|
+
return;
|
|
44
|
+
managed.entry.state = state;
|
|
45
|
+
if (info.error !== undefined)
|
|
46
|
+
managed.entry.error = info.error;
|
|
47
|
+
if (state === 'ready' || state === 'connecting')
|
|
48
|
+
delete managed.entry.error;
|
|
49
|
+
}
|
|
50
|
+
/** Record the current tool count for a registered instance. */
|
|
51
|
+
setToolCount(name, toolCount) {
|
|
52
|
+
const managed = this.managed.get(name);
|
|
53
|
+
if (managed)
|
|
54
|
+
managed.entry.toolCount = toolCount;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Record the eager/deferred tool breakdown for a registered instance.
|
|
58
|
+
* Also overwrites `toolCount` with `eager + deferred`, keeping the
|
|
59
|
+
* invariant `eagerCount + deferredCount === toolCount`.
|
|
60
|
+
*/
|
|
61
|
+
setToolBreakdown(name, breakdown) {
|
|
62
|
+
const managed = this.managed.get(name);
|
|
63
|
+
if (!managed)
|
|
64
|
+
return;
|
|
65
|
+
managed.entry.eagerCount = breakdown.eager;
|
|
66
|
+
managed.entry.deferredCount = breakdown.deferred;
|
|
67
|
+
managed.entry.toolCount = breakdown.eager + breakdown.deferred;
|
|
68
|
+
}
|
|
69
|
+
/** A snapshot of every registered server today. */
|
|
70
|
+
entries() {
|
|
71
|
+
return Array.from(this.managed.values()).map(({ entry }) => ({ ...entry }));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Disconnect a registered server: stop its connection and unregister its
|
|
75
|
+
* tools, leaving the entry marked `disconnected`.
|
|
76
|
+
* @param name - the instance `serverName`.
|
|
77
|
+
* @throws when no such server is registered or its control rejects.
|
|
78
|
+
*/
|
|
79
|
+
async disconnect(name) {
|
|
80
|
+
const managed = this.require(name);
|
|
81
|
+
await managed.control.disconnect();
|
|
82
|
+
this.report(name, 'disconnected');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Reconnect a registered server: tear down and establish a fresh connection.
|
|
86
|
+
* @param name - the instance `serverName`.
|
|
87
|
+
* @throws when no such server is registered or its control rejects.
|
|
88
|
+
*/
|
|
89
|
+
async reconnect(name) {
|
|
90
|
+
const managed = this.require(name);
|
|
91
|
+
this.report(name, 'connecting');
|
|
92
|
+
await managed.control.reconnect();
|
|
93
|
+
}
|
|
94
|
+
require(name) {
|
|
95
|
+
const managed = this.managed.get(name);
|
|
96
|
+
if (!managed)
|
|
97
|
+
throw new Error(`mcpConnections: no server "${name}" is registered`);
|
|
98
|
+
return managed;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource bridge: exposes an MCP server's `resources` capability to the model
|
|
3
|
+
* as two model tools when the server declares the capability and every other
|
|
4
|
+
* pathway is unavailable.
|
|
5
|
+
*
|
|
6
|
+
* The preferred seam for read-only resources is a filesystem Provider, but the
|
|
7
|
+
* `ctx.fs` seam abstracts *real* filesystems over `FsTarget` / `processPath` /
|
|
8
|
+
* `readBytes` — it cannot represent virtual MCP resources (arbitrary URIs with
|
|
9
|
+
* server-owned contents). So this module registers the next best fallback: two
|
|
10
|
+
* server-qualified model tools, `mcp__<server>__list_mcp_resources` and
|
|
11
|
+
* `mcp__<server>__read_mcp_resource`, that call `resources/list` and
|
|
12
|
+
* `resources/read`. They share the tool-generation disposer map so disconnect
|
|
13
|
+
* and re-sync unregister them together.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
19
|
+
import type { JsonValue } from '@dsh-cc/tools';
|
|
20
|
+
/** Registered disposers for the two resource bridges; shared shape with tools. */
|
|
21
|
+
export type ResourceDisposers = Map<string, () => void>;
|
|
22
|
+
/**
|
|
23
|
+
* Register the two resource bridge tools on `ctx.tools` if they are not already
|
|
24
|
+
* present. Registration conflicts (a server tool already owning the public
|
|
25
|
+
* name) are contained and logged; the bridge should never shadow another tool.
|
|
26
|
+
*
|
|
27
|
+
* @param client - connected MCP client.
|
|
28
|
+
* @param ctx - Cordis context providing the `tools` registry.
|
|
29
|
+
* @param serverName - server namespace for the public tool names.
|
|
30
|
+
* @returns the registered disposers (empty when the bridge could not register).
|
|
31
|
+
*/
|
|
32
|
+
export declare function syncResources(client: Client, ctx: Context, serverName: string): ResourceDisposers;
|
|
33
|
+
/**
|
|
34
|
+
* Public name for one resource bridge tool under a server's namespace.
|
|
35
|
+
* @param serverName - server namespace.
|
|
36
|
+
* @param kind - `list_mcp_resources` or `read_mcp_resource`.
|
|
37
|
+
* @returns the model-facing tool name `mcp__<serverName>__<kind>`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resourcePublicName(serverName: string, kind: string): string;
|
|
40
|
+
/** Re-exported canonically for callers that want the URL type. */
|
|
41
|
+
export type { JsonValue };
|
|
42
|
+
//# sourceMappingURL=resources.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAKvE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAE9C,kFAAkF;AAClF,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,CAAA;AAmBvD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,OAAO,EACZ,UAAU,EAAE,MAAM,GACjB,iBAAiB,CAgBnB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3E;AAyED,kEAAkE;AAClE,YAAY,EAAE,SAAS,EAAE,CAAA"}
|
package/lib/resources.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource bridge: exposes an MCP server's `resources` capability to the model
|
|
3
|
+
* as two model tools when the server declares the capability and every other
|
|
4
|
+
* pathway is unavailable.
|
|
5
|
+
*
|
|
6
|
+
* The preferred seam for read-only resources is a filesystem Provider, but the
|
|
7
|
+
* `ctx.fs` seam abstracts *real* filesystems over `FsTarget` / `processPath` /
|
|
8
|
+
* `readBytes` — it cannot represent virtual MCP resources (arbitrary URIs with
|
|
9
|
+
* server-owned contents). So this module registers the next best fallback: two
|
|
10
|
+
* server-qualified model tools, `mcp__<server>__list_mcp_resources` and
|
|
11
|
+
* `mcp__<server>__read_mcp_resource`, that call `resources/list` and
|
|
12
|
+
* `resources/read`. They share the tool-generation disposer map so disconnect
|
|
13
|
+
* and re-sync unregister them together.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { ListResourcesResultSchema, ReadResourceResultSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
18
|
+
/** List a page without the SDK's per-page output-validator cache. */
|
|
19
|
+
function listResourcesUncached(client, cursor) {
|
|
20
|
+
return client.request({ method: 'resources/list', ...cursor === undefined ? {} : { params: { cursor } } }, ListResourcesResultSchema);
|
|
21
|
+
}
|
|
22
|
+
/** Read one resource without the SDK's per-page output-validator cache. */
|
|
23
|
+
function readResourceUncached(client, uri, signal) {
|
|
24
|
+
return client.request({ method: 'resources/read', params: { uri } }, ReadResourceResultSchema, signal === undefined ? {} : { signal });
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Register the two resource bridge tools on `ctx.tools` if they are not already
|
|
28
|
+
* present. Registration conflicts (a server tool already owning the public
|
|
29
|
+
* name) are contained and logged; the bridge should never shadow another tool.
|
|
30
|
+
*
|
|
31
|
+
* @param client - connected MCP client.
|
|
32
|
+
* @param ctx - Cordis context providing the `tools` registry.
|
|
33
|
+
* @param serverName - server namespace for the public tool names.
|
|
34
|
+
* @returns the registered disposers (empty when the bridge could not register).
|
|
35
|
+
*/
|
|
36
|
+
export function syncResources(client, ctx, serverName) {
|
|
37
|
+
const listPublicName = resourcePublicName(serverName, 'list_mcp_resources');
|
|
38
|
+
const readPublicName = resourcePublicName(serverName, 'read_mcp_resource');
|
|
39
|
+
const disposers = new Map();
|
|
40
|
+
const registrations = [
|
|
41
|
+
{ publicName: listPublicName, definition: listDefinition(listPublicName, client, serverName) },
|
|
42
|
+
{ publicName: readPublicName, definition: readDefinition(readPublicName, client) },
|
|
43
|
+
];
|
|
44
|
+
for (const { publicName, definition } of registrations) {
|
|
45
|
+
try {
|
|
46
|
+
disposers.set(publicName, ctx.tools.register(definition));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
ctx.logger.warn(`mcp-client(${serverName}): resource bridge "${publicName}" could not register: ${String(error)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return disposers;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Public name for one resource bridge tool under a server's namespace.
|
|
56
|
+
* @param serverName - server namespace.
|
|
57
|
+
* @param kind - `list_mcp_resources` or `read_mcp_resource`.
|
|
58
|
+
* @returns the model-facing tool name `mcp__<serverName>__<kind>`.
|
|
59
|
+
*/
|
|
60
|
+
export function resourcePublicName(serverName, kind) {
|
|
61
|
+
return `mcp__${serverName}__${kind}`;
|
|
62
|
+
}
|
|
63
|
+
/** ToolDefinition for `resources_list`: enumerate the server's resources. */
|
|
64
|
+
function listDefinition(publicName, client, serverName) {
|
|
65
|
+
return {
|
|
66
|
+
name: publicName,
|
|
67
|
+
description: `List the resources exposed by the MCP server "${serverName}".`,
|
|
68
|
+
parameters: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {},
|
|
71
|
+
},
|
|
72
|
+
output: {
|
|
73
|
+
schema: { type: 'string' },
|
|
74
|
+
render: (_args, value) => [{ type: 'text', text: value }],
|
|
75
|
+
},
|
|
76
|
+
execute: async () => {
|
|
77
|
+
const parts = [];
|
|
78
|
+
let cursor;
|
|
79
|
+
do {
|
|
80
|
+
const page = await listResourcesUncached(client, cursor);
|
|
81
|
+
for (const r of page.resources) {
|
|
82
|
+
parts.push({
|
|
83
|
+
uri: r.uri,
|
|
84
|
+
name: r.name,
|
|
85
|
+
...r.description !== undefined ? { description: r.description } : {},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
cursor = page.nextCursor;
|
|
89
|
+
} while (cursor);
|
|
90
|
+
if (parts.length === 0)
|
|
91
|
+
return '(no resources)';
|
|
92
|
+
return parts.map(r => `${r.uri}${r.name !== undefined ? ' — ' + r.name : ''}`).join('\n');
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** ToolDefinition for `resources_read`: read one resource by URI. */
|
|
97
|
+
function readDefinition(publicName, client) {
|
|
98
|
+
return {
|
|
99
|
+
name: publicName,
|
|
100
|
+
description: 'Read the contents of one MCP resource by its URI.',
|
|
101
|
+
parameters: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
uri: { type: 'string', description: 'The resource URI to read.' },
|
|
105
|
+
},
|
|
106
|
+
required: ['uri'],
|
|
107
|
+
},
|
|
108
|
+
output: {
|
|
109
|
+
schema: { type: 'string' },
|
|
110
|
+
render: (_args, value) => [{ type: 'text', text: value }],
|
|
111
|
+
},
|
|
112
|
+
execute: async (args, exec) => {
|
|
113
|
+
const params = (typeof args === 'object' && args !== null ? args : {});
|
|
114
|
+
const uri = typeof params.uri === 'string' ? params.uri : '';
|
|
115
|
+
if (uri === '')
|
|
116
|
+
throw new Error('read_mcp_resource requires a "uri" argument');
|
|
117
|
+
const result = await readResourceUncached(client, uri, exec.signal);
|
|
118
|
+
const contents = [];
|
|
119
|
+
for (const block of result.contents) {
|
|
120
|
+
// A resource content block is either text-shaped or blob-shaped.
|
|
121
|
+
const item = block;
|
|
122
|
+
if (item.text !== undefined) {
|
|
123
|
+
contents.push(item.text);
|
|
124
|
+
}
|
|
125
|
+
else if (item.blob !== undefined) {
|
|
126
|
+
contents.push(`[binary blob resource: ${item.uri ?? 'unknown'}]`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
contents.push('[unsupported resource content]');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return contents.join('\n') || '(resource returned no contents)';
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=resources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resources.js","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture stdio MCP server stderr so it cannot inherit the parent TTY.
|
|
3
|
+
* SDK default is `stderr: 'inherit'`; piping is mandatory for a TUI host.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
|
+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
9
|
+
/** Ring capacity for the in-memory tail used in connection-failure warns. */
|
|
10
|
+
export declare const STDERR_TAIL_CAPACITY: number;
|
|
11
|
+
/** Max characters of that tail embedded in a single-line `ctx.logger.warn`. */
|
|
12
|
+
export declare const STDERR_WARN_CAPACITY = 1024;
|
|
13
|
+
/** Default size cap for the stdio stderr log; ONE backup generation is kept. */
|
|
14
|
+
export declare const STDIO_LOG_MAX_BYTES: number;
|
|
15
|
+
/**
|
|
16
|
+
* Append-only ring of the newest `capacity` characters. Used so a noisy
|
|
17
|
+
* server cannot grow memory without bound while still leaving a crash banner
|
|
18
|
+
* for the supervisor's warn line.
|
|
19
|
+
*/
|
|
20
|
+
export declare class BoundedTail {
|
|
21
|
+
private readonly capacity;
|
|
22
|
+
private buffer;
|
|
23
|
+
constructor(capacity: number);
|
|
24
|
+
push(chunk: string): void;
|
|
25
|
+
snapshot(): string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Last ≤1 KB of a captured tail, newlines collapsed, for a one-line logger.
|
|
29
|
+
* @returns `undefined` when the tail is empty so callers can omit the suffix.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatStdioStderrForWarn(tail: string): string | undefined;
|
|
32
|
+
/** Snapshot of the ring attached to `transport`, or `''` when none. */
|
|
33
|
+
export declare function stdioStderrTail(transport: Transport): string;
|
|
34
|
+
/**
|
|
35
|
+
* Pipe `transport.stderr`, drain into a 4 KB ring, and append raw bytes to
|
|
36
|
+
* `$DSH_HOME/mcp-logs/<serverName>.log` (or `logDir` when given), rotating at
|
|
37
|
+
* `maxBytes` (default {@link STDIO_LOG_MAX_BYTES}; `maxBytes <= 0` disables)
|
|
38
|
+
* into a single `<serverName>.log.1` backup generation, both at open time and
|
|
39
|
+
* mid-stream.
|
|
40
|
+
*
|
|
41
|
+
* Must run before `start()`: SDK 1.29 creates the PassThrough in the
|
|
42
|
+
* constructor when `stderr: 'pipe'`. An undrained pipe back-pressures the
|
|
43
|
+
* child until it blocks in `write(2)`.
|
|
44
|
+
*
|
|
45
|
+
* No-ops when `stderr` is missing so `vi.fn()` SDK mocks stay constructible.
|
|
46
|
+
*
|
|
47
|
+
* ponytail: rotation is size-based with one backup generation (the previous
|
|
48
|
+
* `.log.1` is discarded on each rotation); concurrent dsh-cc sessions sharing
|
|
49
|
+
* a serverName may interleave (session header marks each generation);
|
|
50
|
+
* WriteStream buffer grows if the disk stalls — log must never block MCP.
|
|
51
|
+
* Documented rotation edges: (a) bytes buffered in the old stream at rotation
|
|
52
|
+
* time flush into `.log.1` after the rename — a crash between rename and
|
|
53
|
+
* flush loses at most one stream buffer; (b) another session holding the
|
|
54
|
+
* renamed `.log.1` writes into an anonymous inode whose bytes vanish on
|
|
55
|
+
* close — worst-case extra disk ≈ maxBytes × live sessions. A failed
|
|
56
|
+
* rotation rename (e.g. `.log.1` is a directory) sets `rotationDisabled` so
|
|
57
|
+
* appending cannot become a rename-retry storm per chunk; appending then
|
|
58
|
+
* grows unbounded, which is exactly the pre-rotation worst case.
|
|
59
|
+
*
|
|
60
|
+
* INVARIANT: all rotation fs ops (`statSync`/`renameSync`/`createWriteStream`)
|
|
61
|
+
* are SYNCHRONOUS and run inside the `data` handler / open path. PassThrough
|
|
62
|
+
* `data` events cannot interleave, and the `end` handler cannot fire
|
|
63
|
+
* mid-rotation; async fs here would reintroduce real races.
|
|
64
|
+
*/
|
|
65
|
+
export declare function attachStdioStderrDrain(transport: StdioClientTransport, serverName: string, logDir?: string, maxBytes?: number): void;
|
|
66
|
+
//# sourceMappingURL=stdio-stderr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio-stderr.d.ts","sourceRoot":"","sources":["../src/stdio-stderr.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AACrF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAA;AAE9E,6EAA6E;AAC7E,eAAO,MAAM,oBAAoB,QAAW,CAAA;AAC5C,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,OAAO,CAAA;AACxC,gFAAgF;AAChF,eAAO,MAAM,mBAAmB,QAAkB,CAAA;AAIlD;;;;GAIG;AACH,qBAAa,WAAW;IAEV,OAAO,CAAC,QAAQ,CAAC,QAAQ;IADrC,OAAO,CAAC,MAAM,CAAK;gBACU,QAAQ,EAAE,MAAM;IAE7C,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAOzB,QAAQ,IAAI,MAAM;CAGnB;AAUD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMzE;AAED,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAE5D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,oBAAoB,EAC/B,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,GAAE,MAA4B,GACrC,IAAI,CAiDN"}
|