@khorsheed/dsh-capability-catalog 0.1.95
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 +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +286 -0
- package/README.zh.md +138 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.js +10100 -0
- package/lib/index.js +4022 -0
- package/lib/invariant.js +25 -0
- package/lib/mcpConnector-C2TbkPBY.js +69 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +1417 -0
- package/lib/typert.remote-client.d.ts +68 -0
- package/lib/typert.remote-client.js +1136 -0
- package/lib/types/capabilities.d.ts +103 -0
- package/lib/types/capabilities.js +116 -0
- package/lib/types/channels.d.ts +41 -0
- package/lib/types/channels.js +75 -0
- package/lib/types/client/AddMcpDialog.d.ts +13 -0
- package/lib/types/client/AddMcpDialog.js +79 -0
- package/lib/types/client/AddSkillModal.d.ts +17 -0
- package/lib/types/client/AddSkillModal.js +138 -0
- package/lib/types/client/BundleFileTree.d.ts +7 -0
- package/lib/types/client/BundleFileTree.js +74 -0
- package/lib/types/client/CapabilityCatalogCard.d.ts +57 -0
- package/lib/types/client/CapabilityCatalogCard.js +479 -0
- package/lib/types/client/CredentialField.d.ts +14 -0
- package/lib/types/client/CredentialField.js +9 -0
- package/lib/types/client/McpCards.d.ts +13 -0
- package/lib/types/client/McpCards.js +12 -0
- package/lib/types/client/McpServerManageModal.d.ts +18 -0
- package/lib/types/client/McpServerManageModal.js +63 -0
- package/lib/types/client/MetadataRow.d.ts +10 -0
- package/lib/types/client/MetadataRow.js +6 -0
- package/lib/types/client/ModalShell.d.ts +16 -0
- package/lib/types/client/ModalShell.js +27 -0
- package/lib/types/client/ModeChips.d.ts +38 -0
- package/lib/types/client/ModeChips.js +42 -0
- package/lib/types/client/SchemaView.d.ts +11 -0
- package/lib/types/client/SchemaView.js +143 -0
- package/lib/types/client/SkillCards.d.ts +28 -0
- package/lib/types/client/SkillCards.js +43 -0
- package/lib/types/client/SkillDetailModal.d.ts +69 -0
- package/lib/types/client/SkillDetailModal.js +167 -0
- package/lib/types/client/ToolCards.d.ts +28 -0
- package/lib/types/client/ToolCards.js +52 -0
- package/lib/types/client/ToolDetailModal.d.ts +12 -0
- package/lib/types/client/ToolDetailModal.js +29 -0
- package/lib/types/client/icons.d.ts +26 -0
- package/lib/types/client/icons.js +10 -0
- package/lib/types/client/index.d.ts +21 -0
- package/lib/types/client/index.js +196 -0
- package/lib/types/client/locales.d.ts +392 -0
- package/lib/types/client/locales.js +395 -0
- package/lib/types/client/mcp-model.d.ts +24 -0
- package/lib/types/client/mcp-model.js +87 -0
- package/lib/types/client/mode-model.d.ts +108 -0
- package/lib/types/client/mode-model.js +140 -0
- package/lib/types/client/preset-display.d.ts +21 -0
- package/lib/types/client/preset-display.js +20 -0
- package/lib/types/client/settle.d.ts +40 -0
- package/lib/types/client/settle.js +57 -0
- package/lib/types/client/slots.d.ts +86 -0
- package/lib/types/client/slots.js +1 -0
- package/lib/types/envHint.d.ts +22 -0
- package/lib/types/envHint.js +76 -0
- package/lib/types/import.d.ts +106 -0
- package/lib/types/import.js +635 -0
- package/lib/types/index.d.ts +216 -0
- package/lib/types/index.js +739 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +23 -0
- package/lib/types/mcpConnector.d.ts +24 -0
- package/lib/types/mcpConnector.js +73 -0
- package/lib/types/mcpStore.d.ts +68 -0
- package/lib/types/mcpStore.js +235 -0
- package/lib/types/mcpTools.d.ts +45 -0
- package/lib/types/mcpTools.js +125 -0
- package/lib/types/mcps.d.ts +64 -0
- package/lib/types/mcps.js +157 -0
- package/lib/types/modes.d.ts +65 -0
- package/lib/types/modes.js +97 -0
- package/lib/types/namespace.d.ts +9 -0
- package/lib/types/namespace.js +8 -0
- package/lib/types/official-tools.d.ts +3 -0
- package/lib/types/official-tools.js +44 -0
- package/lib/types/preset-scope.d.ts +105 -0
- package/lib/types/preset-scope.js +117 -0
- package/lib/types/remote.d.ts +48 -0
- package/lib/types/remote.js +120 -0
- package/lib/types/scoped-delivery.d.ts +243 -0
- package/lib/types/scoped-delivery.js +686 -0
- package/lib/types/scoped-edits.d.ts +90 -0
- package/lib/types/scoped-edits.js +234 -0
- package/lib/types/settings.d.ts +28 -0
- package/lib/types/settings.js +28 -0
- package/lib/types/shellEnv.d.ts +65 -0
- package/lib/types/shellEnv.js +232 -0
- package/lib/types/skills.d.ts +103 -0
- package/lib/types/skills.js +303 -0
- package/lib/types/tool-origin.d.ts +29 -0
- package/lib/types/tool-origin.js +30 -0
- package/lib/types/types.d.ts +315 -0
- package/lib/types/types.js +8 -0
- package/lib/types/zip.d.ts +24 -0
- package/lib/types/zip.js +80 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +119 -0
- package/scripts/gen-official-tools.mts +52 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@khorsheed/dsh-capability-catalog`.
|
|
3
|
+
* @module @khorsheed/dsh-capability-catalog/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "capability-catalog-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@khorsheed/dsh-capability-catalog`.
|
|
3
|
+
* @module @khorsheed/dsh-capability-catalog/invariant
|
|
4
|
+
*/
|
|
5
|
+
const PACKAGE_NAME = '@khorsheed/dsh-capability-catalog';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export const name = 'capability-catalog-invariant';
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export const inject = ['invariants'];
|
|
10
|
+
/**
|
|
11
|
+
* No runtime invariant: the catalog is read-only over existing registries
|
|
12
|
+
* (ctx.skills / ctx.tools) and the add-skill write path validates its inputs
|
|
13
|
+
* before touching disk. The unit tests exercise the channel attribution,
|
|
14
|
+
* skill decoding, and the wire projection.
|
|
15
|
+
*/
|
|
16
|
+
const install = () => { };
|
|
17
|
+
/**
|
|
18
|
+
* Register this package's invariant companion.
|
|
19
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
20
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
21
|
+
*/
|
|
22
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
23
|
+
/* jscpd:ignore-end */
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP connector backed by the official `@modelcontextprotocol/sdk` (the same
|
|
3
|
+
* library `@deepseek-ai/dsh-mcp-client` uses). Kept as a thin, swappable seam:
|
|
4
|
+
* the catalog owns the connection lifecycle (connect → list → register tools →
|
|
5
|
+
* call → close) and registers discovered tools on `ctx.tools`. If upstream later
|
|
6
|
+
* exposes a stable dynamic `connect(ctx, config)`, swap this adapter for it.
|
|
7
|
+
*
|
|
8
|
+
* v1 scope: stdio + streamable-http connect, paginated `tools/list`,
|
|
9
|
+
* `tools/call` passthrough, and close. Reconnect/atomic-replace/tool-filter are
|
|
10
|
+
* follow-ups.
|
|
11
|
+
* @module @khorsheed/dsh-capability-catalog/mcpConnector
|
|
12
|
+
*/
|
|
13
|
+
import type { CatalogMcpServerConfig, CatalogMcpTool } from './types.ts';
|
|
14
|
+
/** One live MCP connection. */
|
|
15
|
+
export interface McpConnection {
|
|
16
|
+
readonly tools: readonly CatalogMcpTool[];
|
|
17
|
+
call: (toolName: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
18
|
+
close: () => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/** Resolve a secretRef marker to its stored value (the catalog's ctx.credentials). */
|
|
21
|
+
export type ResolveSecret = (ref: string) => Promise<string | undefined>;
|
|
22
|
+
/** Connect, discover all tools (follow pagination), and return a live handle. */
|
|
23
|
+
export declare function connectMcpServer(config: CatalogMcpServerConfig): Promise<McpConnection>;
|
|
24
|
+
//# sourceMappingURL=mcpConnector.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP connector backed by the official `@modelcontextprotocol/sdk` (the same
|
|
3
|
+
* library `@deepseek-ai/dsh-mcp-client` uses). Kept as a thin, swappable seam:
|
|
4
|
+
* the catalog owns the connection lifecycle (connect → list → register tools →
|
|
5
|
+
* call → close) and registers discovered tools on `ctx.tools`. If upstream later
|
|
6
|
+
* exposes a stable dynamic `connect(ctx, config)`, swap this adapter for it.
|
|
7
|
+
*
|
|
8
|
+
* v1 scope: stdio + streamable-http connect, paginated `tools/list`,
|
|
9
|
+
* `tools/call` passthrough, and close. Reconnect/atomic-replace/tool-filter are
|
|
10
|
+
* follow-ups.
|
|
11
|
+
* @module @khorsheed/dsh-capability-catalog/mcpConnector
|
|
12
|
+
*/
|
|
13
|
+
// The SDK is a third-party host dep the gen-typert overlay cannot resolve
|
|
14
|
+
// (only @deepseek-ai/* + harness node_modules). @ts-ignore keeps generation
|
|
15
|
+
// green; at runtime the host resolves these from node_modules.
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
22
|
+
/** Build the transport for a config (secrets already resolved to plaintext). */
|
|
23
|
+
function makeTransport(config) {
|
|
24
|
+
if (config.transport === 'stdio') {
|
|
25
|
+
const env = {};
|
|
26
|
+
for (const [k, v] of config.env ?? [])
|
|
27
|
+
env[k] = v;
|
|
28
|
+
return new StdioClientTransport({
|
|
29
|
+
command: config.command ?? 'npx',
|
|
30
|
+
args: [...(config.args ?? [])],
|
|
31
|
+
...(Object.keys(env).length > 0 ? { env } : {}),
|
|
32
|
+
...(config.cwd ? { cwd: config.cwd } : {}),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// streamable-http: the SDK transport takes headers via `requestInit` — without
|
|
36
|
+
// them an auth-bearing server (e.g. `Authorization`, `x-api-key`) is never
|
|
37
|
+
// authenticated. Plaintext header values arrive here after secret resolution.
|
|
38
|
+
const headers = config.headers ?? [];
|
|
39
|
+
if (headers.length > 0) {
|
|
40
|
+
return new StreamableHTTPClientTransport(new URL(config.url ?? ''), {
|
|
41
|
+
requestInit: { headers: Object.fromEntries(headers) },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return new StreamableHTTPClientTransport(new URL(config.url ?? ''));
|
|
45
|
+
}
|
|
46
|
+
/** Map an SDK tool to the catalog's tool row (enabled by default). */
|
|
47
|
+
function toCatalogTool(sdkTool) {
|
|
48
|
+
return {
|
|
49
|
+
name: sdkTool.name,
|
|
50
|
+
description: sdkTool.description ?? '',
|
|
51
|
+
...(sdkTool.inputSchema !== undefined ? { parameters: sdkTool.inputSchema } : {}),
|
|
52
|
+
enabled: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Connect, discover all tools (follow pagination), and return a live handle. */
|
|
56
|
+
export async function connectMcpServer(config) {
|
|
57
|
+
const client = new Client({ name: 'dsh-capability-catalog', version: '0.1.0' });
|
|
58
|
+
const transport = makeTransport(config);
|
|
59
|
+
await client.connect(transport);
|
|
60
|
+
const tools = [];
|
|
61
|
+
let cursor;
|
|
62
|
+
do {
|
|
63
|
+
const page = await client.listTools(cursor === undefined ? undefined : { cursor });
|
|
64
|
+
for (const t of page.tools)
|
|
65
|
+
tools.push(toCatalogTool(t));
|
|
66
|
+
cursor = page.nextCursor;
|
|
67
|
+
} while (cursor !== undefined);
|
|
68
|
+
return {
|
|
69
|
+
tools,
|
|
70
|
+
call: async (toolName, args) => client.callTool({ name: toolName, arguments: args }),
|
|
71
|
+
close: async () => { await client.close().catch(() => { }); },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side MCP server store: keeps the user's configured MCP servers, their
|
|
3
|
+
* discovered tools, credential state, and enable flags. Persistence is a pluggable
|
|
4
|
+
* seam: the host hands the store an `onPersist` callback (backed by the settings
|
|
5
|
+
* namespace) and seeds it with `loadFrom` at boot, so servers survive a restart.
|
|
6
|
+
* Tool discovery connects via the SDK connector and caches the tool list.
|
|
7
|
+
*/
|
|
8
|
+
import type { CatalogMcpCredentialDecl, CatalogMcpServerConfig, CatalogMcpTool } from './types.ts';
|
|
9
|
+
/** One tool's persisted record: the user's toggle choice only. Descriptions and
|
|
10
|
+
* parameter schemas are discovered at connect time and deliberately NOT persisted
|
|
11
|
+
* (they bloat settings.yaml and go stale); `reconnectAll` re-fills them at boot. */
|
|
12
|
+
export interface PersistedMcpTool {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly enabled: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Wire/persisted form of one stored server (config + enabled + tool toggles). */
|
|
17
|
+
export interface PersistedMcpServer {
|
|
18
|
+
readonly config: CatalogMcpServerConfig;
|
|
19
|
+
readonly tools: readonly PersistedMcpTool[];
|
|
20
|
+
}
|
|
21
|
+
/** The full persisted MCP state block (stored under the settings namespace `mcp` key). */
|
|
22
|
+
export interface PersistedMcpState {
|
|
23
|
+
readonly servers?: readonly PersistedMcpServer[];
|
|
24
|
+
/** Namespaced credential ref -> value (the user's own config document). */
|
|
25
|
+
readonly credentials?: Readonly<Record<string, string>>;
|
|
26
|
+
}
|
|
27
|
+
/** The catalog's own MCP management store (one per process). */
|
|
28
|
+
export declare class McpStore {
|
|
29
|
+
private readonly servers;
|
|
30
|
+
/** Persistence hook, set by the host after the settings scope is available. */
|
|
31
|
+
onPersist: (() => void) | undefined;
|
|
32
|
+
/** Seed the store from the persisted state (boot or external config edit). */
|
|
33
|
+
loadFrom(state: PersistedMcpState | undefined): void;
|
|
34
|
+
/** Serialize the current state for persistence. */
|
|
35
|
+
toPersisted(): PersistedMcpState;
|
|
36
|
+
private persist;
|
|
37
|
+
list(): CatalogMcpServerConfig[];
|
|
38
|
+
/** Discovered tool list per server (name → tools). */
|
|
39
|
+
toolsByServer(): Record<string, readonly CatalogMcpTool[]>;
|
|
40
|
+
/** Credential declarations for all servers (whether configured, never the value). */
|
|
41
|
+
credentials(): CatalogMcpCredentialDecl[];
|
|
42
|
+
/** Add / replace a server config (keeps its credentials if already present). */
|
|
43
|
+
add(config: CatalogMcpServerConfig): void;
|
|
44
|
+
remove(serverName: string): void;
|
|
45
|
+
/** Set server-level enable flag (discovery/registration is the caller's job). */
|
|
46
|
+
setEnabled(serverName: string, enabled: boolean): void;
|
|
47
|
+
/** Set one credential value (empty clears it). */
|
|
48
|
+
setCredential(ref: string, value: string): void;
|
|
49
|
+
/** Configure a tool's enable flag. */
|
|
50
|
+
setToolEnabled(serverName: string, toolName: string, enabled: boolean): void;
|
|
51
|
+
/** Connect + discover tools for a server; caches the list (connect errors surface). */
|
|
52
|
+
discover(serverName: string): Promise<readonly CatalogMcpTool[]>;
|
|
53
|
+
/** Re-connect + re-discover every enabled server — boot-time refill of the
|
|
54
|
+
* discovered descriptions/parameters (the persisted block stores only
|
|
55
|
+
* name+enabled). Preserves per-tool enable flags; per-server errors are
|
|
56
|
+
* recorded and do not stop the others. */
|
|
57
|
+
reconnectAll(): Promise<void>;
|
|
58
|
+
/** A server's most recent connection/dev error. */
|
|
59
|
+
errorOf(serverName: string): string | undefined;
|
|
60
|
+
/** Forward one tool call to a server (connect → call → close). Resolves secrets. */
|
|
61
|
+
callTool(serverName: string, rawName: string, args: Record<string, unknown>): Promise<unknown>;
|
|
62
|
+
/** Extract credential reference paths from a config (env/header/url-query).
|
|
63
|
+
* Namespaced `mcp.<serverName>.env.<KEY>` so multiple servers don't collide. */
|
|
64
|
+
private credentialRefsOf;
|
|
65
|
+
}
|
|
66
|
+
/** Whether a config value contains an unresolved secret marker (bare or scheme-prefixed). */
|
|
67
|
+
export declare function isSecretRef(value: string): boolean;
|
|
68
|
+
//# sourceMappingURL=mcpStore.d.ts.map
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side MCP server store: keeps the user's configured MCP servers, their
|
|
3
|
+
* discovered tools, credential state, and enable flags. Persistence is a pluggable
|
|
4
|
+
* seam: the host hands the store an `onPersist` callback (backed by the settings
|
|
5
|
+
* namespace) and seeds it with `loadFrom` at boot, so servers survive a restart.
|
|
6
|
+
* Tool discovery connects via the SDK connector and caches the tool list.
|
|
7
|
+
*/
|
|
8
|
+
import { resolveConfigSecrets } from './mcps.js';
|
|
9
|
+
/** The catalog's own MCP management store (one per process). */
|
|
10
|
+
export class McpStore {
|
|
11
|
+
servers = new Map();
|
|
12
|
+
/** Persistence hook, set by the host after the settings scope is available. */
|
|
13
|
+
onPersist = undefined;
|
|
14
|
+
/** Seed the store from the persisted state (boot or external config edit). */
|
|
15
|
+
loadFrom(state) {
|
|
16
|
+
if (state === undefined)
|
|
17
|
+
return;
|
|
18
|
+
for (const server of state.servers ?? []) {
|
|
19
|
+
const existing = this.servers.get(server.config.serverName);
|
|
20
|
+
// Keep descriptions/parameters already discovered in memory: the persisted
|
|
21
|
+
// block is authoritative only for the per-tool enabled flag (it stores just
|
|
22
|
+
// name+enabled), so a reload — e.g. after our own persist round-trips through
|
|
23
|
+
// the settings watch — must merge the toggle rather than wipe the tools.
|
|
24
|
+
const known = new Map((existing?.tools ?? []).map(t => [t.name, t]));
|
|
25
|
+
const tools = (server.tools ?? []).map(t => {
|
|
26
|
+
const prev = known.get(t.name);
|
|
27
|
+
return prev !== undefined
|
|
28
|
+
? { ...prev, enabled: t.enabled }
|
|
29
|
+
: { name: t.name, description: '', enabled: t.enabled };
|
|
30
|
+
});
|
|
31
|
+
this.servers.set(server.config.serverName, {
|
|
32
|
+
config: server.config,
|
|
33
|
+
credentials: existing?.credentials ?? new Map(),
|
|
34
|
+
tools,
|
|
35
|
+
error: undefined,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
for (const [ref, value] of Object.entries(state.credentials ?? {})) {
|
|
39
|
+
const s = this.servers.get(refServer(ref));
|
|
40
|
+
if (s !== undefined)
|
|
41
|
+
s.credentials.set(ref, value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Serialize the current state for persistence. */
|
|
45
|
+
toPersisted() {
|
|
46
|
+
const servers = [...this.servers.values()]
|
|
47
|
+
.sort((a, b) => a.config.serverName.localeCompare(b.config.serverName))
|
|
48
|
+
.map(s => ({ config: s.config, tools: s.tools.map(t => ({ name: t.name, enabled: t.enabled })) }));
|
|
49
|
+
const credentials = {};
|
|
50
|
+
for (const s of this.servers.values())
|
|
51
|
+
for (const [ref, value] of s.credentials)
|
|
52
|
+
credentials[ref] = value;
|
|
53
|
+
return { servers, credentials };
|
|
54
|
+
}
|
|
55
|
+
persist() {
|
|
56
|
+
this.onPersist?.();
|
|
57
|
+
}
|
|
58
|
+
list() {
|
|
59
|
+
return [...this.servers.values()].map(s => ({ ...s.config, enabled: s.config.enabled })).sort((a, b) => a.serverName.localeCompare(b.serverName));
|
|
60
|
+
}
|
|
61
|
+
/** Discovered tool list per server (name → tools). */
|
|
62
|
+
toolsByServer() {
|
|
63
|
+
const out = {};
|
|
64
|
+
for (const [name, s] of this.servers)
|
|
65
|
+
out[name] = s.tools;
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
/** Credential declarations for all servers (whether configured, never the value). */
|
|
69
|
+
credentials() {
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const [name, s] of this.servers) {
|
|
72
|
+
for (const ref of this.credentialRefsOf(s.config)) {
|
|
73
|
+
out.push({ ref, label: refLabel(ref), configured: s.credentials.has(ref) });
|
|
74
|
+
}
|
|
75
|
+
void name;
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/** Add / replace a server config (keeps its credentials if already present). */
|
|
80
|
+
add(config) {
|
|
81
|
+
const existing = this.servers.get(config.serverName);
|
|
82
|
+
this.servers.set(config.serverName, {
|
|
83
|
+
config,
|
|
84
|
+
credentials: existing?.credentials ?? new Map(),
|
|
85
|
+
tools: existing?.tools ?? [],
|
|
86
|
+
error: existing?.error,
|
|
87
|
+
});
|
|
88
|
+
this.persist();
|
|
89
|
+
}
|
|
90
|
+
remove(serverName) {
|
|
91
|
+
if (this.servers.delete(serverName))
|
|
92
|
+
this.persist();
|
|
93
|
+
}
|
|
94
|
+
/** Set server-level enable flag (discovery/registration is the caller's job). */
|
|
95
|
+
setEnabled(serverName, enabled) {
|
|
96
|
+
const s = this.servers.get(serverName);
|
|
97
|
+
if (s === undefined)
|
|
98
|
+
return;
|
|
99
|
+
s.config = { ...s.config, enabled };
|
|
100
|
+
this.persist();
|
|
101
|
+
}
|
|
102
|
+
/** Set one credential value (empty clears it). */
|
|
103
|
+
setCredential(ref, value) {
|
|
104
|
+
const serverName = refServer(ref);
|
|
105
|
+
const s = this.servers.get(serverName);
|
|
106
|
+
if (s === undefined)
|
|
107
|
+
return;
|
|
108
|
+
if (value === '')
|
|
109
|
+
s.credentials.delete(ref);
|
|
110
|
+
else
|
|
111
|
+
s.credentials.set(ref, value);
|
|
112
|
+
this.persist();
|
|
113
|
+
}
|
|
114
|
+
/** Configure a tool's enable flag. */
|
|
115
|
+
setToolEnabled(serverName, toolName, enabled) {
|
|
116
|
+
const s = this.servers.get(serverName);
|
|
117
|
+
if (s === undefined)
|
|
118
|
+
return;
|
|
119
|
+
s.tools = s.tools.map(t => (t.name === toolName ? { ...t, enabled } : t));
|
|
120
|
+
this.persist();
|
|
121
|
+
}
|
|
122
|
+
/** Connect + discover tools for a server; caches the list (connect errors surface). */
|
|
123
|
+
async discover(serverName) {
|
|
124
|
+
const s = this.servers.get(serverName);
|
|
125
|
+
if (s === undefined)
|
|
126
|
+
return [];
|
|
127
|
+
try {
|
|
128
|
+
// Dynamic import keeps the SDK-backed connector out of the typert-analyzed
|
|
129
|
+
// host face (third-party SDK types aren't resolvable in the gen-typert overlay).
|
|
130
|
+
const { connectMcpServer } = await import('./mcpConnector.js');
|
|
131
|
+
// Resolve secretRef markers to the stored credential values before connecting,
|
|
132
|
+
// so a configured API key is sent as its real value (never the secretRef).
|
|
133
|
+
const resolved = await resolveConfigSecrets(s.config, async (ref) => {
|
|
134
|
+
const candidates = [
|
|
135
|
+
`mcp.${serverName}.env.${ref}`,
|
|
136
|
+
`mcp.${serverName}.header.${ref}`,
|
|
137
|
+
`mcp.${serverName}.query.${ref}`,
|
|
138
|
+
];
|
|
139
|
+
for (const candidate of candidates) {
|
|
140
|
+
const value = s.credentials.get(candidate);
|
|
141
|
+
if (value !== undefined)
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
});
|
|
146
|
+
const connection = await connectMcpServer(resolved);
|
|
147
|
+
// Preserve the user's per-tool enable flags — re-discovering refreshes
|
|
148
|
+
// descriptions/parameters without resetting toggles (new tools default on).
|
|
149
|
+
const prevEnabled = new Map(s.tools.map(t => [t.name, t.enabled]));
|
|
150
|
+
s.tools = connection.tools.map(t => ({ ...t, enabled: prevEnabled.get(t.name) ?? true }));
|
|
151
|
+
s.error = undefined;
|
|
152
|
+
await connection.close();
|
|
153
|
+
this.persist();
|
|
154
|
+
return s.tools;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
s.error = String(error);
|
|
158
|
+
this.persist();
|
|
159
|
+
return s.tools;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Re-connect + re-discover every enabled server — boot-time refill of the
|
|
163
|
+
* discovered descriptions/parameters (the persisted block stores only
|
|
164
|
+
* name+enabled). Preserves per-tool enable flags; per-server errors are
|
|
165
|
+
* recorded and do not stop the others. */
|
|
166
|
+
async reconnectAll() {
|
|
167
|
+
for (const server of this.list()) {
|
|
168
|
+
if (!server.enabled)
|
|
169
|
+
continue;
|
|
170
|
+
await this.discover(server.serverName);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** A server's most recent connection/dev error. */
|
|
174
|
+
errorOf(serverName) {
|
|
175
|
+
return this.servers.get(serverName)?.error;
|
|
176
|
+
}
|
|
177
|
+
/** Forward one tool call to a server (connect → call → close). Resolves secrets. */
|
|
178
|
+
async callTool(serverName, rawName, args) {
|
|
179
|
+
const s = this.servers.get(serverName);
|
|
180
|
+
if (s === undefined)
|
|
181
|
+
throw new Error(`unknown MCP server: ${serverName}`);
|
|
182
|
+
const resolved = await resolveConfigSecrets(s.config, async (ref) => {
|
|
183
|
+
const candidates = [
|
|
184
|
+
`mcp.${serverName}.env.${ref}`,
|
|
185
|
+
`mcp.${serverName}.header.${ref}`,
|
|
186
|
+
`mcp.${serverName}.query.${ref}`,
|
|
187
|
+
];
|
|
188
|
+
for (const candidate of candidates) {
|
|
189
|
+
const value = s.credentials.get(candidate);
|
|
190
|
+
if (value !== undefined)
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
});
|
|
195
|
+
const { connectMcpServer } = await import('./mcpConnector.js');
|
|
196
|
+
const connection = await connectMcpServer(resolved);
|
|
197
|
+
try {
|
|
198
|
+
return await connection.call(rawName, args);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
await connection.close();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** Extract credential reference paths from a config (env/header/url-query).
|
|
205
|
+
* Namespaced `mcp.<serverName>.env.<KEY>` so multiple servers don't collide. */
|
|
206
|
+
credentialRefsOf(config) {
|
|
207
|
+
const refs = [];
|
|
208
|
+
for (const [k, v] of config.env ?? [])
|
|
209
|
+
if (isSecretRef(v))
|
|
210
|
+
refs.push(`mcp.${config.serverName}.env.${k}`);
|
|
211
|
+
for (const [k, v] of config.headers ?? [])
|
|
212
|
+
if (isSecretRef(v))
|
|
213
|
+
refs.push(`mcp.${config.serverName}.header.${k}`);
|
|
214
|
+
if (config.url !== undefined) {
|
|
215
|
+
for (const m of config.url.matchAll(/secretRef:([A-Za-z0-9_.-]+)/g)) {
|
|
216
|
+
refs.push(`mcp.${config.serverName}.query.${m[1]}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return refs;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** Whether a config value contains an unresolved secret marker (bare or scheme-prefixed). */
|
|
223
|
+
export function isSecretRef(value) {
|
|
224
|
+
return value.includes('secretRef:');
|
|
225
|
+
}
|
|
226
|
+
/** Human label for a namespaced ref (strip the `mcp.<server>.` prefix). */
|
|
227
|
+
function refLabel(ref) {
|
|
228
|
+
const idx = ref.indexOf('.');
|
|
229
|
+
return ref.slice(idx + 1);
|
|
230
|
+
}
|
|
231
|
+
/** The server name a namespaced credential ref belongs to (`mcp.<server>…`). */
|
|
232
|
+
function refServer(ref) {
|
|
233
|
+
const m = /^mcp\.([A-Za-z0-9_-]+)\./.exec(ref);
|
|
234
|
+
return m?.[1] ?? '';
|
|
235
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import type { CatalogJsonValue, CatalogMcpTool } from './types.ts';
|
|
3
|
+
/** Deterministic model-facing name for `(serverName, rawName)`. */
|
|
4
|
+
export declare function publicToolName(serverName: string, rawName: string): string;
|
|
5
|
+
/** A tool-invocation forwarder (connects, resolves secrets, calls, closes). */
|
|
6
|
+
export type McpToolCallFn = (serverName: string, rawName: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
7
|
+
/** Extract human-readable text from MCP content blocks (text blocks joined). */
|
|
8
|
+
export declare function extractText(content: readonly unknown[] | undefined): string;
|
|
9
|
+
/** Build a harness `ToolDefinition` for one discovered MCP tool. */
|
|
10
|
+
export declare function mcpToolDefinition(opts: {
|
|
11
|
+
publicName: string;
|
|
12
|
+
serverName: string;
|
|
13
|
+
rawName: string;
|
|
14
|
+
description: string;
|
|
15
|
+
parameters: CatalogJsonValue | undefined;
|
|
16
|
+
callTool: McpToolCallFn;
|
|
17
|
+
}): ToolDefinition;
|
|
18
|
+
/** One desired registration during reconcile. */
|
|
19
|
+
export interface DesiredMcpTool {
|
|
20
|
+
readonly publicName: string;
|
|
21
|
+
readonly serverName: string;
|
|
22
|
+
readonly rawName: string;
|
|
23
|
+
readonly description: string;
|
|
24
|
+
readonly parameters: CatalogJsonValue | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The tools registry seam. IMPORTANT: `register` must be invoked as a MEMBER
|
|
28
|
+
* call on `tools` (the traceable proxy) — never extracted to a standalone
|
|
29
|
+
* variable — so `this.ctx` (effect/scope ownership) resolves correctly.
|
|
30
|
+
*/
|
|
31
|
+
export interface McpToolRegistry {
|
|
32
|
+
register(definition: ToolDefinition): () => void;
|
|
33
|
+
onRegisterError?: (error: unknown) => void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Reconcile registered MCP tools: dispose every previously-registered tool, then
|
|
37
|
+
* register the desired set. Registration failures are contained per tool.
|
|
38
|
+
*/
|
|
39
|
+
export declare function reconcileRegisteredMcpTools(desired: readonly DesiredMcpTool[], registry: McpToolRegistry, current: Map<string, () => void>, callTool: McpToolCallFn): void;
|
|
40
|
+
/** Compute the desired set from enabled servers + their enabled tools. */
|
|
41
|
+
export declare function desiredMcpTools(servers: readonly {
|
|
42
|
+
readonly serverName: string;
|
|
43
|
+
readonly enabled: boolean;
|
|
44
|
+
}[], toolsByServer: Readonly<Record<string, readonly CatalogMcpTool[]>>): DesiredMcpTool[];
|
|
45
|
+
//# sourceMappingURL=mcpTools.d.ts.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool bridge: turns catalog-discovered MCP tools into harness `ToolDefinition`s
|
|
3
|
+
* registered on `ctx.tools` so the model can call them. A bounded subset of the
|
|
4
|
+
* official mcp-client bridge — the common text-result case (no image/attachment
|
|
5
|
+
* projection, no task execution). Model-facing names follow the stable
|
|
6
|
+
* `mcp__<serverName>__<rawName>` contract, normalized to the DeepSeek
|
|
7
|
+
* function-name constraints; the raw name is only ever sent on the wire.
|
|
8
|
+
*
|
|
9
|
+
* Registration must always invoke `ctx.tools.register(definition)` as a MEMBER
|
|
10
|
+
* call on the traceable service proxy (never extract `register` to a standalone
|
|
11
|
+
* variable) — the proxy resolves `this.ctx` (effect/scope ownership) from the
|
|
12
|
+
* calling context, which a bare function reference loses.
|
|
13
|
+
* @module @khorsheed/dsh-capability-catalog/mcpTools
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
/** DeepSeek function-name contract: at most 64 characters. */
|
|
17
|
+
const MAX_PUBLIC_NAME_LENGTH = 64;
|
|
18
|
+
/** DeepSeek function-name contract: only `[A-Za-z0-9_-]` is allowed. */
|
|
19
|
+
const INVALID_NAME_CHARS = /[^A-Za-z0-9_-]/g;
|
|
20
|
+
/** Hex chars of the SHA-256 identity hash appended on lossy normalization. */
|
|
21
|
+
const HASH_LENGTH = 12;
|
|
22
|
+
/** Deterministic model-facing name for `(serverName, rawName)`. */
|
|
23
|
+
export function publicToolName(serverName, rawName) {
|
|
24
|
+
const joined = `mcp__${serverName}__${rawName}`;
|
|
25
|
+
const normalized = joined.replace(INVALID_NAME_CHARS, '_');
|
|
26
|
+
if (normalized === joined && normalized.length <= MAX_PUBLIC_NAME_LENGTH)
|
|
27
|
+
return normalized;
|
|
28
|
+
const hash = createHash('sha256').update(joined).digest('hex').slice(0, HASH_LENGTH);
|
|
29
|
+
return `${normalized.slice(0, MAX_PUBLIC_NAME_LENGTH - HASH_LENGTH - 1)}_${hash}`;
|
|
30
|
+
}
|
|
31
|
+
/** Coerce a raw MCP inputSchema into a JSON Schema object the registry accepts. */
|
|
32
|
+
function sanitizeParameters(parameters) {
|
|
33
|
+
if (parameters !== null && typeof parameters === 'object' && !Array.isArray(parameters)) {
|
|
34
|
+
return parameters;
|
|
35
|
+
}
|
|
36
|
+
return { type: 'object', properties: {} };
|
|
37
|
+
}
|
|
38
|
+
/** Map the SDK `callTool` result to the canonical `{ content, structuredContent }`. */
|
|
39
|
+
function normalizeResult(result) {
|
|
40
|
+
const r = (result ?? {});
|
|
41
|
+
if (r.isError === true)
|
|
42
|
+
throw new Error(r.error ?? 'MCP tool reported an error');
|
|
43
|
+
return {
|
|
44
|
+
content: Array.isArray(r.content) ? r.content : [],
|
|
45
|
+
...(r.structuredContent !== undefined ? { structuredContent: r.structuredContent } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Extract human-readable text from MCP content blocks (text blocks joined). */
|
|
49
|
+
export function extractText(content) {
|
|
50
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
51
|
+
const texts = blocks
|
|
52
|
+
.filter((b) => typeof b === 'object' && b !== null && b.type === 'text' && typeof b.text === 'string')
|
|
53
|
+
.map((b) => b.text);
|
|
54
|
+
return texts.length > 0 ? texts.join('\n') : JSON.stringify(blocks);
|
|
55
|
+
}
|
|
56
|
+
/** Build a harness `ToolDefinition` for one discovered MCP tool. */
|
|
57
|
+
export function mcpToolDefinition(opts) {
|
|
58
|
+
const { publicName, rawName, serverName, description, parameters, callTool } = opts;
|
|
59
|
+
return {
|
|
60
|
+
name: publicName,
|
|
61
|
+
description,
|
|
62
|
+
parameters: sanitizeParameters(parameters),
|
|
63
|
+
output: {
|
|
64
|
+
schema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: { content: { type: 'array', items: {} } },
|
|
67
|
+
required: ['content'],
|
|
68
|
+
additionalProperties: true,
|
|
69
|
+
},
|
|
70
|
+
render: (_args, value) => {
|
|
71
|
+
const result = value;
|
|
72
|
+
return [{ type: 'text', text: extractText(result.content) }];
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
execute: async (args) => {
|
|
76
|
+
const result = await callTool(serverName, rawName, (args ?? {}));
|
|
77
|
+
return normalizeResult(result);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Reconcile registered MCP tools: dispose every previously-registered tool, then
|
|
83
|
+
* register the desired set. Registration failures are contained per tool.
|
|
84
|
+
*/
|
|
85
|
+
export function reconcileRegisteredMcpTools(desired, registry, current, callTool) {
|
|
86
|
+
for (const dispose of [...current.values()])
|
|
87
|
+
dispose();
|
|
88
|
+
current.clear();
|
|
89
|
+
for (const d of desired) {
|
|
90
|
+
try {
|
|
91
|
+
const dispose = registry.register(mcpToolDefinition({
|
|
92
|
+
publicName: d.publicName,
|
|
93
|
+
serverName: d.serverName,
|
|
94
|
+
rawName: d.rawName,
|
|
95
|
+
description: d.description,
|
|
96
|
+
parameters: d.parameters,
|
|
97
|
+
callTool,
|
|
98
|
+
}));
|
|
99
|
+
current.set(d.publicName, dispose);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
registry.onRegisterError?.(error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Compute the desired set from enabled servers + their enabled tools. */
|
|
107
|
+
export function desiredMcpTools(servers, toolsByServer) {
|
|
108
|
+
const desired = [];
|
|
109
|
+
for (const server of servers) {
|
|
110
|
+
if (!server.enabled)
|
|
111
|
+
continue;
|
|
112
|
+
for (const tool of toolsByServer[server.serverName] ?? []) {
|
|
113
|
+
if (!tool.enabled)
|
|
114
|
+
continue;
|
|
115
|
+
desired.push({
|
|
116
|
+
publicName: publicToolName(server.serverName, tool.name),
|
|
117
|
+
serverName: server.serverName,
|
|
118
|
+
rawName: tool.name,
|
|
119
|
+
description: tool.description,
|
|
120
|
+
parameters: tool.parameters,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return desired;
|
|
125
|
+
}
|