@dsh-libing/mcp-manager 0.0.0-stage → 0.1.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.
@@ -0,0 +1,19 @@
1
+ /** Serial profile edits, cancellable observations/probes and official reconciliation. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ import type { Mutation, Outcome, Selection, State } from './shared.ts';
4
+ import { ToolPolicy } from './policy.ts';
5
+ export declare class McpManager {
6
+ private ctx;
7
+ readonly policy: ToolPolicy;
8
+ private tail;
9
+ private closing;
10
+ private abort;
11
+ private reads;
12
+ private tests;
13
+ constructor(ctx: Context);
14
+ start(): Promise<void>;
15
+ private observe;
16
+ state(selection: Selection, signal?: AbortSignal): Promise<State>;
17
+ mutate(operation: Mutation, signal?: AbortSignal): Promise<Outcome>;
18
+ dispose(): Promise<void>;
19
+ }
@@ -0,0 +1,178 @@
1
+ import { readProfilePatches, reconcileProfilePatches } from '@deepseek-ai/dsh-app-boot';
2
+ import { withFileLock } from '@deepseek-ai/dsh-atomic-write';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { isMap, isSeq } from 'yaml';
5
+ import { MCP } from "./shared.js";
6
+ import { commit, document, hash, ManagerError, readOwnedFile, replaceBlock, rowsIn } from "./storage.js";
7
+ import { appendRow, rawManaged, serverRecords, writeConfig } from "./servers.js";
8
+ import { ToolPolicy } from "./policy.js";
9
+ import { directory, selectionView, withToolView } from "./view.js";
10
+ import { probe } from "./probe.js";
11
+ export class McpManager {
12
+ ctx;
13
+ policy;
14
+ tail = Promise.resolve();
15
+ closing = false;
16
+ abort = new AbortController();
17
+ reads = new Set();
18
+ tests = new Map();
19
+ constructor(ctx) {
20
+ this.ctx = ctx;
21
+ this.policy = new ToolPolicy(ctx, ctx.profileContext.dir);
22
+ }
23
+ async start() {
24
+ await this.policy.start();
25
+ }
26
+ observe(signal, action) {
27
+ if (this.closing)
28
+ return Promise.reject(new ManagerError('Plugin is unloading', 503));
29
+ const task = action(signal ? AbortSignal.any([signal, this.abort.signal]) : this.abort.signal);
30
+ this.reads.add(task);
31
+ void task.then(() => this.reads.delete(task), () => this.reads.delete(task));
32
+ return task;
33
+ }
34
+ state(selection, signal) {
35
+ return this.observe(signal, async (signal) => {
36
+ await this.policy.refresh();
37
+ const selected = await selectionView(this.ctx, selection, signal);
38
+ return await withToolView(this.ctx, selected.selectedSessionId, signal, async (view) => {
39
+ this.policy.scan();
40
+ const text = await readOwnedFile(this.ctx.profileContext.patchPath), records = await serverRecords(this.ctx, text);
41
+ return {
42
+ profile: this.ctx.profileContext.name,
43
+ patchRevision: hash(text),
44
+ policyRevision: this.policy.revision,
45
+ ...selected,
46
+ scopeKind: view.kind,
47
+ presetId: view.presetId,
48
+ tools: directory(this.ctx, view, records, this.policy),
49
+ servers: records.map((record) => ({
50
+ ...record.item,
51
+ tested: this.tests.get(hash(`${record.item.id}\0${JSON.stringify(record.raw)}`)) ?? null
52
+ })),
53
+ notices: view.kind === 'global'
54
+ ? ['Only the global registry is observed; session visibility is unknown']
55
+ : view.kind === 'preset'
56
+ ? ['Current preset definition is observed; no Agent was created']
57
+ : []
58
+ };
59
+ });
60
+ });
61
+ }
62
+ mutate(operation, signal) {
63
+ if (this.closing)
64
+ return Promise.reject(new ManagerError('Plugin is unloading', 503));
65
+ const task = this.tail.then(async () => {
66
+ if (this.closing)
67
+ throw new ManagerError('Plugin is unloading', 503);
68
+ if (operation.kind === 'policy') {
69
+ const state = await this.state(operation.selection, signal);
70
+ if (!state.tools.some((tool) => tool.name === operation.name))
71
+ throw new ManagerError('Tool no longer exists in this view', 409);
72
+ await this.policy.set(operation.name, operation.enabled, operation.revision);
73
+ return { application: 'applied' };
74
+ }
75
+ if (operation.kind === 'probe')
76
+ return await this.observe(signal, async (signal) => {
77
+ const text = await readOwnedFile(this.ctx.profileContext.patchPath);
78
+ if (hash(text) !== operation.revision)
79
+ throw new ManagerError('Configuration changed; refresh and retry', 409);
80
+ const entry = (await serverRecords(this.ctx, text)).find((r) => r.item.id === operation.id);
81
+ if (!entry)
82
+ throw new ManagerError('MCP entry no longer exists', 404);
83
+ const raw = entry.patchId ? rawManaged(text, entry.patchId) : entry.raw;
84
+ const outcome = await probe(raw, signal);
85
+ this.tests.set(hash(`${entry.item.id}\0${JSON.stringify(entry.raw)}`), outcome);
86
+ return {
87
+ application: 'tested',
88
+ notice: outcome.ok
89
+ ? 'Independent connection test succeeded'
90
+ : 'Independent connection test failed; check configuration and local DSH diagnostics'
91
+ };
92
+ });
93
+ if (operation.kind === 'enable') {
94
+ const text = await readOwnedFile(this.ctx.profileContext.patchPath);
95
+ if (hash(text) !== operation.revision)
96
+ throw new ManagerError('Configuration changed; refresh and retry', 409);
97
+ const entry = (await serverRecords(this.ctx, text)).find((r) => r.item.id === operation.id);
98
+ const official = (await this.ctx.pluginManager.listPlugins()).find((r) => r.entryId === entry?.entry?.id);
99
+ if (!entry?.item.toggleable || !official)
100
+ throw new ManagerError('Entry is controlled by its configuration source', 409);
101
+ const result = await this.ctx.pluginManager.setPluginEnabled(official.entryId, operation.enabled);
102
+ return {
103
+ application: result.application === 'cancelled' ? 'failed' : result.application,
104
+ ...(result.error ? { notice: 'Official plugin manager could not apply this change' } : {})
105
+ };
106
+ }
107
+ return await withFileLock(`${this.ctx.profileContext.dir}/package.json`, async () => {
108
+ const action = async () => {
109
+ const path = this.ctx.profileContext.patchPath, text = await readOwnedFile(path);
110
+ if (hash(text) !== operation.revision)
111
+ throw new ManagerError('Configuration changed; refresh and retry', 409);
112
+ const entries = await serverRecords(this.ctx, text);
113
+ let body;
114
+ if (operation.kind === 'create') {
115
+ if (entries.some((r) => r.item.scope === 'profile' && r.item.serverName === operation.config.serverName))
116
+ throw new ManagerError('Server namespace already exists', 409);
117
+ body = appendRow(text, `libing-mcp-${randomUUID()}`, operation.config);
118
+ }
119
+ else {
120
+ const entry = entries.find((r) => r.item.id === operation.id);
121
+ if (!entry?.patchId || !entry.item.editable)
122
+ throw new ManagerError('This configuration is read-only', 409);
123
+ if (entry.item.enabled)
124
+ throw new ManagerError('Disable the service before editing or deleting its configuration', 409);
125
+ if (operation.kind === 'update' &&
126
+ operation.config.serverName !== entry.item.serverName)
127
+ throw new ManagerError('Server namespace cannot be changed; create a new service');
128
+ const { doc, rows } = rowsIn(text), index = rows?.items.findIndex((r) => isMap(r) && r.get('id') === entry.patchId) ?? -1;
129
+ if (!rows || index < 0)
130
+ throw new ManagerError('Managed entry changed', 409);
131
+ if (operation.kind === 'update')
132
+ writeConfig(doc, [0, 'insert', index, 'config'], operation.config);
133
+ else
134
+ rows.delete(index);
135
+ body = String(doc);
136
+ if (operation.kind === 'delete') {
137
+ // Check external rows before removing our insertion.
138
+ const block = replaceBlock(text, body), outer = document(block);
139
+ if (!isSeq(outer.contents))
140
+ throw new ManagerError('Invalid patch');
141
+ const foreign = outer.contents.items.filter((item) => isMap(item) && item.get('id') === entry.patchId);
142
+ if (foreign.some((item) => (isMap(item) &&
143
+ item.items.some((pair) => !['id', 'name', 'disabled'].includes(String(pair.key)))) ||
144
+ (isMap(item) && item.has('name') && item.get('name') !== MCP)))
145
+ throw new ManagerError('External override exists; remove it at its source first', 409);
146
+ // AST serialization only for removing official overrides would change foreign bytes;
147
+ // refuse instead of rewriting them. A disabled override is harmless after removal.
148
+ }
149
+ }
150
+ await commit(path, text, replaceBlock(text, body));
151
+ const hmr = this.ctx.get('hmr');
152
+ if (!hmr)
153
+ return { application: 'restart-required' };
154
+ try {
155
+ await reconcileProfilePatches(this.ctx.root, readProfilePatches('dsh', this.ctx.profileContext), 'dsh');
156
+ return { application: 'applied' };
157
+ }
158
+ catch {
159
+ return {
160
+ application: 'failed',
161
+ notice: 'Configuration saved, but the official loader could not apply it; refresh or restart DSH'
162
+ };
163
+ }
164
+ };
165
+ const hmr = this.ctx.get('hmr');
166
+ return hmr ? await hmr.runExclusive(action) : await action();
167
+ }, { waitMs: 10000 });
168
+ });
169
+ this.tail = task.catch(() => { });
170
+ return task;
171
+ }
172
+ async dispose() {
173
+ this.closing = true;
174
+ this.abort.abort();
175
+ await Promise.allSettled([this.tail, ...this.reads]);
176
+ this.policy.dispose();
177
+ }
178
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /** Connection owns authentication and origin checks; this context owns all routes. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ export declare const name = "libing-mcp-manager";
4
+ export declare const inject: string[];
5
+ export declare function apply(ctx: Context): Promise<void>;
package/lib/index.js ADDED
@@ -0,0 +1,54 @@
1
+ import { z } from 'zod';
2
+ import { McpManager } from "./controller.js";
3
+ import { API, mutationSchema, selectionSchema } from "./shared.js";
4
+ import { ManagerError, message } from "./storage.js";
5
+ export const name = 'libing-mcp-manager';
6
+ export const inject = [
7
+ 'connection',
8
+ 'tools',
9
+ 'sessionQuery',
10
+ 'pluginManager',
11
+ 'profileContext',
12
+ 'loader'
13
+ ];
14
+ export async function apply(ctx) {
15
+ const manager = new McpManager(ctx);
16
+ ctx.effect(() => () => manager.dispose(), 'libing-mcp-manager: operations');
17
+ await manager.start();
18
+ const route = (path, method, action) => ctx.effect(() => ctx.connection.fetch.register({
19
+ path: `${API}/${path}`,
20
+ methods: [method],
21
+ requestBody: 'buffered',
22
+ fetch: async (request) => {
23
+ try {
24
+ return Response.json(await action(request), {
25
+ headers: { 'cache-control': 'no-store' }
26
+ });
27
+ }
28
+ catch (error) {
29
+ return Response.json({ error: message(error) }, {
30
+ status: error instanceof ManagerError
31
+ ? error.status
32
+ : error instanceof z.ZodError || error instanceof SyntaxError
33
+ ? 400
34
+ : 500,
35
+ headers: { 'cache-control': 'no-store' }
36
+ });
37
+ }
38
+ }
39
+ }), `libing-mcp-manager: ${path}`);
40
+ route('state', 'GET', async (request) => {
41
+ const entries = [...new URL(request.url).searchParams];
42
+ if (new Set(entries.map(([key]) => key)).size !== entries.length)
43
+ throw new ManagerError('Duplicate query parameter');
44
+ return await manager.state(selectionSchema.parse(Object.fromEntries(entries)), request.signal);
45
+ });
46
+ route('mutate', 'POST', async (request) => {
47
+ if (request.headers.get('content-type')?.split(';')[0]?.trim() !== 'application/json')
48
+ throw new ManagerError('Expected application/json', 415);
49
+ const bytes = await request.arrayBuffer();
50
+ if (bytes.byteLength > 65536)
51
+ throw new ManagerError('Request exceeds byte limit', 413);
52
+ return await manager.mutate(mutationSchema.parse(JSON.parse(new TextDecoder().decode(bytes))), request.signal);
53
+ });
54
+ }
@@ -0,0 +1,19 @@
1
+ /** Profile-wide monotonic denial; no mutation of a ToolRuntime's private registry. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ export declare const protectedTool: (name: string) => name is "run_code" | "respond";
4
+ export declare class ToolPolicy {
5
+ private ctx;
6
+ private denied;
7
+ private raw;
8
+ private disposers;
9
+ private closing;
10
+ readonly file: string;
11
+ constructor(ctx: Context, dir: string);
12
+ start(): Promise<void>;
13
+ scan(): void;
14
+ allowed(name: string): boolean;
15
+ get revision(): string;
16
+ refresh(): Promise<void>;
17
+ set(name: string, enabled: boolean, revision: string): Promise<void>;
18
+ dispose(): void;
19
+ }
package/lib/policy.js ADDED
@@ -0,0 +1,99 @@
1
+ import ToolRuntime from '@deepseek-ai/dsh-tools';
2
+ import { z } from 'zod';
3
+ import { join } from 'node:path';
4
+ import { withFileLock } from '@deepseek-ai/dsh-atomic-write';
5
+ import { commit, hash, ManagerError, readOwnedFile } from "./storage.js";
6
+ export const protectedTool = (name) => name === 'run_code' || name === 'respond';
7
+ const schema = z.object({ denied: z.array(z.string().min(1).max(512)).max(10000) }).strict();
8
+ export class ToolPolicy {
9
+ ctx;
10
+ denied = new Set();
11
+ raw = '';
12
+ disposers = new Map();
13
+ closing = false;
14
+ file;
15
+ constructor(ctx, dir) {
16
+ this.ctx = ctx;
17
+ this.file = join(dir, 'libing-tool-policy.json');
18
+ }
19
+ async start() {
20
+ await this.refresh();
21
+ this.scan();
22
+ // A Service announces itself during super(), before its fields initialize.
23
+ this.ctx.on('internal/service', (name) => {
24
+ if (name === 'tools')
25
+ queueMicrotask(() => {
26
+ if (!this.closing)
27
+ this.scan();
28
+ });
29
+ }, { global: true });
30
+ // Covers a synchronously published service before its queued observer, and
31
+ // installs guards before the registry performs its monotonic guard stage.
32
+ this.ctx.on('tools/pre-execute', async (exec, next) => {
33
+ this.scan();
34
+ return this.allowed(exec.name)
35
+ ? await next()
36
+ : { kind: 'deny', reason: 'Disabled in LiBing MCP Center' };
37
+ }, { global: true, prepend: true });
38
+ }
39
+ scan() {
40
+ const current = new Set();
41
+ for (const key of Object.getOwnPropertySymbols(this.ctx.reflect.store)) {
42
+ const value = this.ctx.reflect.store[key]?.value;
43
+ if (!(value instanceof ToolRuntime))
44
+ continue;
45
+ current.add(value);
46
+ if (this.disposers.has(value))
47
+ continue;
48
+ const owned = this.ctx.reflect.trace(value);
49
+ const dispose = owned.guard((exec) => this.allowed(exec.name) ? undefined : 'Disabled in LiBing MCP Center');
50
+ this.disposers.set(value, dispose);
51
+ }
52
+ for (const [runtime, dispose] of this.disposers)
53
+ if (!current.has(runtime)) {
54
+ dispose();
55
+ this.disposers.delete(runtime);
56
+ }
57
+ }
58
+ allowed(name) {
59
+ return !this.denied.has(name);
60
+ }
61
+ get revision() {
62
+ return hash(this.raw);
63
+ }
64
+ async refresh() {
65
+ await withFileLock(this.file, async () => {
66
+ const raw = await readOwnedFile(this.file), rules = schema.parse(raw ? JSON.parse(raw) : { denied: [] });
67
+ if (rules.denied.some(protectedTool))
68
+ throw new ManagerError('Policy cannot deny internal orchestration tools', 409);
69
+ this.raw = raw;
70
+ this.denied = new Set(rules.denied);
71
+ });
72
+ }
73
+ async set(name, enabled, revision) {
74
+ if (protectedTool(name))
75
+ throw new ManagerError('Internal orchestration tools are protected');
76
+ return await withFileLock(this.file, async () => {
77
+ if (this.closing)
78
+ throw new ManagerError('Plugin is unloading', 503);
79
+ const before = await readOwnedFile(this.file);
80
+ if (hash(before) !== revision || before !== this.raw)
81
+ throw new ManagerError('Policy changed; refresh and retry', 409);
82
+ const next = new Set(this.denied);
83
+ if (enabled)
84
+ next.delete(name);
85
+ else
86
+ next.add(name);
87
+ const raw = JSON.stringify({ denied: [...next].sort() }, null, 2) + '\n';
88
+ await commit(this.file, before, raw);
89
+ this.raw = raw;
90
+ this.denied = next;
91
+ });
92
+ }
93
+ dispose() {
94
+ this.closing = true;
95
+ for (const dispose of this.disposers.values())
96
+ dispose();
97
+ this.disposers.clear();
98
+ }
99
+ }
package/lib/probe.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { ServerItem } from './shared.ts';
2
+ export declare function probe(raw: Record<string, unknown>, signal: AbortSignal, budget?: number): Promise<NonNullable<ServerItem['tested']>>;
package/lib/probe.js ADDED
@@ -0,0 +1,58 @@
1
+ /** Explicit, independent initialize/listTools probe, with one bounded lifetime. */
2
+ import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
3
+ import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
4
+ import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess';
5
+ import { resolvedConfig } from "./servers.js";
6
+ export async function probe(raw, signal, budget = 8000) {
7
+ const config = resolvedConfig(raw), start = Date.now(), abort = AbortSignal.any([signal, AbortSignal.timeout(budget)]);
8
+ abort.throwIfAborted();
9
+ const client = new Client({ name: 'libing-mcp-probe', version: '0.1.0' }, { capabilities: {} });
10
+ const transport = config.transport === 'stdio'
11
+ ? new StdioClientTransport({
12
+ command: config.command,
13
+ args: config.args,
14
+ cwd: config.cwd,
15
+ env: { ...scrubbedParentEnv(), ...config.credentials },
16
+ stderr: 'ignore'
17
+ })
18
+ : new StreamableHTTPClientTransport(new URL(config.url), {
19
+ requestInit: { headers: config.credentials, signal: abort }
20
+ });
21
+ let stopped;
22
+ const close = () => (stopped ??= (async () => {
23
+ await transport.close().catch(() => { });
24
+ await client.close().catch(() => { });
25
+ })());
26
+ const stop = () => {
27
+ void close();
28
+ };
29
+ abort.addEventListener('abort', stop, { once: true });
30
+ let rejectAbort;
31
+ const aborted = new Promise((_, reject) => {
32
+ rejectAbort = reject;
33
+ });
34
+ const reject = () => rejectAbort?.(abort.reason);
35
+ abort.addEventListener('abort', reject, { once: true });
36
+ const work = (async () => {
37
+ await client.connect(transport);
38
+ return await client.listTools();
39
+ })();
40
+ try {
41
+ const tools = await Promise.race([work, aborted]);
42
+ return {
43
+ ok: true,
44
+ at: new Date().toISOString(),
45
+ durationMs: Date.now() - start,
46
+ toolCount: tools.tools.length
47
+ };
48
+ }
49
+ catch {
50
+ return { ok: false, at: new Date().toISOString(), durationMs: Date.now() - start };
51
+ }
52
+ finally {
53
+ abort.removeEventListener('abort', stop);
54
+ abort.removeEventListener('abort', reject);
55
+ await close();
56
+ await work.catch(() => { });
57
+ }
58
+ }
@@ -0,0 +1,3 @@
1
+ export declare const declaredProviders: Readonly<Record<string, readonly string[]>>;
2
+ /** Official delegation and workflow plugins support a configurable toolName. */
3
+ export declare function declaresTool(owner: string, config: unknown, name: string): boolean;
@@ -0,0 +1,75 @@
1
+ /** Tool contracts verified against the installed DSH 0.2.0-rc.2 packages.
2
+ * A name is only a candidate: view.ts also checks the active entry, service,
3
+ * scope and exact resolved definition. Unknown third-party names stay unknown.
4
+ */
5
+ const contracts = {
6
+ '@deepseek-ai/dsh-tool-ask-user': ['ask_user_question'],
7
+ '@deepseek-ai/dsh-tool-bash': ['bash'],
8
+ '@deepseek-ai/dsh-tool-bash-persistent': ['bash'],
9
+ '@deepseek-ai/dsh-tool-pwsh': ['pwsh'],
10
+ '@deepseek-ai/dsh-tool-pwsh-persistent': ['pwsh'],
11
+ '@deepseek-ai/dsh-tool-fs': ['edit', 'read', 'read_image', 'write'],
12
+ '@deepseek-ai/dsh-tool-fs-search': ['glob', 'grep'],
13
+ '@deepseek-ai/dsh-tool-jobs': ['job_kill', 'job_list', 'job_output'],
14
+ '@deepseek-ai/dsh-tool-skill': ['skill'],
15
+ '@deepseek-ai/dsh-tool-todo': ['todo_write'],
16
+ '@deepseek-ai/dsh-tool-goal': ['create_goal', 'get_goal', 'update_goal'],
17
+ '@deepseek-ai/dsh-tool-web': ['web_fetch', 'web_search'],
18
+ '@deepseek-ai/dsh-tool-cordis': ['cordis_inspect_list', 'cordis_inspect_query'],
19
+ '@deepseek-ai/dsh-tool-present': ['present'],
20
+ '@deepseek-ai/dsh-tool-ralph': ['ralph'],
21
+ '@deepseek-ai/dsh-tool-str-replace-editor': ['str_replace_editor'],
22
+ '@deepseek-ai/dsh-tool-workspace-dependencies': ['load_workspace_dependencies'],
23
+ '@deepseek-ai/dsh-tool-subagent': ['list_subagent_models'],
24
+ '@deepseek-ai/dsh-tool-subagent-control': ['interrupt_agent', 'send_message'],
25
+ '@deepseek-ai/dsh-tool-subagent-control/list-agents': ['list_agents'],
26
+ '@deepseek-ai/dsh-experimental-tool-agent-team': [
27
+ 'interrupt_agent',
28
+ 'list_agents',
29
+ 'send_message',
30
+ 'spawn_teammate',
31
+ 'team_task_create',
32
+ 'team_task_get',
33
+ 'team_task_list',
34
+ 'team_task_update',
35
+ 'wait_agent'
36
+ ],
37
+ '@deepseek-ai/dsh-plan-mode': ['exit_plan_mode'],
38
+ '@deepseek-ai/dsh-schedule': [
39
+ 'schedule_create',
40
+ 'schedule_delete',
41
+ 'schedule_list',
42
+ 'schedule_update'
43
+ ],
44
+ '@dsh-libing/hello': ['libing_hello'],
45
+ '@dsh-libing/knowledge-base': [
46
+ 'libing_kb_search',
47
+ 'libing_kb_build',
48
+ 'libing_kb_catalog',
49
+ 'libing_kb_job',
50
+ 'libing_kb_read_image',
51
+ 'libing_kb_read'
52
+ ]
53
+ };
54
+ export const declaredProviders = Object.fromEntries([...new Set(Object.values(contracts).flat())].map((name) => [
55
+ name,
56
+ Object.entries(contracts)
57
+ .filter(([, names]) => names.includes(name))
58
+ .map(([owner]) => owner)
59
+ ]));
60
+ /** Official delegation and workflow plugins support a configurable toolName. */
61
+ export function declaresTool(owner, config, name) {
62
+ if (declaredProviders[name]?.includes(owner))
63
+ return true;
64
+ const defaultName = owner === '@deepseek-ai/dsh-tool-subagent'
65
+ ? 'subagent'
66
+ : owner === '@deepseek-ai/dsh-tool-workflow'
67
+ ? 'workflow'
68
+ : undefined;
69
+ if (defaultName === undefined)
70
+ return false;
71
+ const toolName = typeof config === 'object' && config !== null && 'toolName' in config
72
+ ? config.toolName
73
+ : undefined;
74
+ return name === (typeof toolName === 'string' ? toolName : defaultName);
75
+ }
@@ -0,0 +1,24 @@
1
+ /** MCP identity is a scope plus entry, never just a server namespace. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ import type { Entry } from '@deepseek-ai/cordis-plugin-loader';
4
+ import type { ScopeKey } from '@deepseek-ai/dsh-scope';
5
+ import type { ServerConfig, ServerItem } from './shared.ts';
6
+ import { document } from './storage.ts';
7
+ export interface ServerRecord {
8
+ item: ServerItem;
9
+ entry?: Entry;
10
+ patchId?: string;
11
+ scopeKey?: ScopeKey;
12
+ raw: Record<string, unknown>;
13
+ }
14
+ export declare function record(value: unknown): Record<string, unknown>;
15
+ export declare function publicConfig(raw: Record<string, unknown>): ServerConfig | null;
16
+ export declare function serverRecords(ctx: Context, text: string): Promise<ServerRecord[]>;
17
+ /** Keep secret nodes by default. Only constrained generated references use !!js. */
18
+ export declare function writeConfig(node: ReturnType<typeof document>, path: (string | number)[], config: ServerConfig): void;
19
+ /** Resolve only literal values or the exact ENV expression this editor emits. */
20
+ export declare function resolvedConfig(raw: Record<string, unknown>): ServerConfig & {
21
+ credentials: Record<string, string>;
22
+ };
23
+ export declare function rawManaged(text: string, id: string): Record<string, unknown>;
24
+ export declare function appendRow(text: string, id: string, config: ServerConfig): string;