archgraph-argo 0.26.2 → 0.28.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.
@@ -1,141 +1,227 @@
1
- // dsh-argo-workspace - generated by install-argo.ps1 (single source of truth:
2
- // the installer; this file is a deployment artifact, do not edit by hand).
3
- //
4
- // Direct MCP bridge to the argo server for DeepSeek Harness. Registers every
5
- // argo tool as mcp__argo__* and injects the current session's workspace
6
- // directory (SessionHeader.cwd) as the per-call `workspaceRoot` argument, so
7
- // one dsh instance follows whichever workspace the user switched to - the
8
- // model sees no extra parameters and no internal tool names. The argo server
9
- // honors the injected workspaceRoot unconditionally.
10
- //
11
- // Zero dependencies on purpose: implements the minimal MCP stdio client
12
- // (JSON-RPC 2.0, one JSON object per line) with Node built-ins only, so the
13
- // plugin runs from any DSH layout without resolving @modelcontextprotocol/sdk.
14
- import { spawn } from 'node:child_process'
15
- import readline from 'node:readline'
16
- import { fileURLToPath } from 'node:url'
17
-
18
- export const name = 'dsh-argo-workspace'
19
- export const inject = ['tools']
20
-
21
- // 包内默认:<pkg>/argo/scripts/argo-mcp-server.js —— 随包走,跨机器可移植。
22
- const DEFAULT_SERVER_PATH = fileURLToPath(
23
- new URL('../argo/scripts/argo-mcp-server.js', import.meta.url),
24
- )
25
-
26
- /** Minimal MCP stdio client over the argo server process. */
27
- function createArgoClient(serverPath, env, cwd) {
28
- const child = spawn('node', [serverPath], {
29
- stdio: ['pipe', 'pipe', 'pipe'],
30
- env,
31
- ...(cwd ? { cwd } : {}),
32
- })
33
- const pending = new Map()
34
- let nextId = 1
35
- readline.createInterface({ input: child.stdout }).on('line', (line) => {
36
- let message
37
- try { message = JSON.parse(line) } catch { return }
38
- if (message && typeof message.id === 'number' && pending.has(message.id)) {
39
- const { resolve, reject } = pending.get(message.id)
40
- pending.delete(message.id)
41
- if (message.error) reject(new Error(JSON.stringify(message.error)))
42
- else resolve(message.result)
43
- }
44
- })
45
- child.stderr.on('data', () => {}) // drain; the argo server logs to stderr
46
- const request = (method, params) => new Promise((resolve, reject) => {
47
- const id = nextId++
48
- pending.set(id, { resolve, reject })
49
- child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n')
50
- })
51
- return {
52
- request,
53
- notify: (method, params) => {
54
- child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n')
55
- },
56
- close: () => child.kill(),
57
- }
58
- }
59
-
60
- /** Connect, list tools, register them, and keep the client until disposal. */
61
- export async function apply(ctx, config) {
62
- const serverPath = config.serverPath
63
- ?? process.env.ARGO_SERVER_PATH
64
- ?? DEFAULT_SERVER_PATH
65
- const workspaces = Array.isArray(config.workspaces) ? config.workspaces : []
66
- const env = { ...process.env }
67
- if (workspaces.length > 0) env.ARGO_WORKSPACE_ROOTS = workspaces.join(';')
68
- const client = createArgoClient(serverPath, env, config.cwd)
69
- ctx.effect(() => () => client.close(), 'dsh-argo-workspace.dispose()')
70
-
71
- let tools = []
72
- try {
73
- await client.request('initialize', {
74
- protocolVersion: '2024-11-05',
75
- capabilities: {},
76
- clientInfo: { name: 'dsh-argo-workspace', version: '0.1.0' },
77
- })
78
- client.notify('notifications/initialized', {})
79
- const listed = await client.request('tools/list', {})
80
- tools = (listed && Array.isArray(listed.tools)) ? listed.tools : []
81
- } catch (error) {
82
- console.warn(`[dsh-argo-workspace] failed to connect to the argo MCP server (${serverPath}): ${error && error.message ? error.message : error}`)
83
- return
84
- }
85
-
86
- const disposers = []
87
- for (const tool of tools) {
88
- const publicName = 'mcp__argo__' + tool.name
89
- disposers.push(ctx.tools.register({
90
- name: publicName,
91
- description: tool.description ?? '',
92
- parameters: tool.inputSchema,
93
- output: {
94
- schema: {
95
- type: 'object',
96
- properties: {
97
- content: { type: 'array', items: {} },
98
- structuredContent: {},
99
- },
100
- required: ['content'],
101
- additionalProperties: false,
102
- },
103
- render: (_args, value) => value.content,
104
- },
105
- execute: async (args, exec) => {
106
- // SessionHeader.cwd (the durable session workspace), NOT
107
- // requestHeader() — that returns the request EpochHeader
108
- // (config/system/tools) which has no cwd field.
109
- const sessionCwd = exec && exec.agent && exec.agent.session
110
- ? exec.agent.session.header?.cwd
111
- : undefined
112
- const injected = { ...(args && typeof args === 'object' ? args : {}) }
113
- if (typeof sessionCwd === 'string' && sessionCwd !== '') {
114
- injected.workspaceRoot = sessionCwd
115
- }
116
- if (exec && exec.signal && exec.signal.aborted) {
117
- throw new Error('aborted')
118
- }
119
- const result = await client.request('tools/call', {
120
- name: tool.name,
121
- arguments: injected,
122
- })
123
- const text = Array.isArray(result.content)
124
- ? result.content
125
- .map((block) => block && block.type === 'text' && typeof block.text === 'string'
126
- ? block.text
127
- : JSON.stringify(block))
128
- .join('\n')
129
- : (result.toolResult !== undefined ? JSON.stringify(result.toolResult) : '(no output)')
130
- if (result.isError === true) throw new Error(text)
131
- return {
132
- content: [{ type: 'text', text }],
133
- ...(result.structuredContent !== undefined ? { structuredContent: result.structuredContent } : {}),
134
- }
135
- },
136
- }))
137
- }
138
- ctx.effect(() => () => {
139
- for (const dispose of disposers) dispose()
140
- }, 'dsh-argo-workspace.tools')
141
- }
1
+ // dsh-argo-workspace - single source of truth for the DSH workspace bridge.
2
+ // install-argo.ps1 copies this file verbatim into ~/.dsh/plugins, so the
3
+ // deployed plugin and this shipped bundle artifact are byte-identical.
4
+ //
5
+ // Direct MCP stdio bridge(es) for DeepSeek Harness. Registers every tool of each
6
+ // configured MCP server as mcp__<id>__* and, for the argo server, injects the
7
+ // current session's workspace directory (SessionHeader.cwd) as the per-call
8
+ // `workspaceRoot` argument, so one dsh instance follows whichever workspace the
9
+ // user switched to - the model sees no extra parameters and no internal tool
10
+ // names. The argo server honors the injected workspaceRoot unconditionally.
11
+ //
12
+ // Multiple servers are supported so a host that can only spawn local stdio
13
+ // children (DSH) can still mount the remote federation Graph MCP through the
14
+ // stdio bridge (GRAPH_MCP_URL). Configure via `config.servers` (a list); the
15
+ // legacy single-server shape (`config.serverPath`) is still honored.
16
+ //
17
+ // Zero dependencies on purpose: implements the minimal MCP stdio client
18
+ // (JSON-RPC 2.0, one JSON object per line) with Node built-ins only, so the
19
+ // plugin runs from any DSH layout without resolving @modelcontextprotocol/sdk.
20
+ import { spawn } from 'node:child_process'
21
+ import readline from 'node:readline'
22
+ import { fileURLToPath } from 'node:url'
23
+
24
+ export const name = 'dsh-argo-workspace'
25
+ export const inject = ['tools']
26
+
27
+ // 包内默认:<pkg>/argo/scripts/argo-mcp-server.js —— 随包走,跨机器可移植。
28
+ const DEFAULT_SERVER_PATH = fileURLToPath(
29
+ new URL('../argo/scripts/argo-mcp-server.js', import.meta.url),
30
+ )
31
+
32
+ // A `serverPath`/`args` entry that starts with ./ or ../ resolves relative to this
33
+ // plugin module (portable bundle references); an absolute path is used as-is.
34
+ function resolveMaybeRelative(value) {
35
+ if (typeof value === 'string' && /^[.][\\/]/.test(value)) {
36
+ try {
37
+ return fileURLToPath(new URL(value, import.meta.url))
38
+ } catch {
39
+ return value
40
+ }
41
+ }
42
+ return value
43
+ }
44
+
45
+ /** Normalize config into a list of server specs (backward compatible). */
46
+ function normalizeServerSpecs(config) {
47
+ const topWorkspaces = Array.isArray(config.workspaces) ? config.workspaces : []
48
+ const withCommon = (spec) => {
49
+ const env = { ...process.env, ...(spec.env || {}) }
50
+ if (spec.injectWorkspaceRoot && topWorkspaces.length > 0) {
51
+ env.ARGO_WORKSPACE_ROOTS = topWorkspaces.join(';')
52
+ }
53
+ // Assert the broker-controlled workspace channel: the argo server will only
54
+ // trust the per-call sessionWorkspaceRoot (which this bridge sets from the
55
+ // durable SessionHeader.cwd, overwriting any model value) when this flag is
56
+ // present. The model cannot set a server process env var, so the trust is
57
+ // unforgeable.
58
+ if (spec.injectWorkspaceRoot) {
59
+ env.ARGO_WORKSPACE_ROOT_TRUSTED = '1'
60
+ }
61
+ return { ...spec, env, cwd: spec.cwd ?? (spec.injectWorkspaceRoot ? config.cwd : undefined) }
62
+ }
63
+
64
+ if (Array.isArray(config.servers) && config.servers.length > 0) {
65
+ return config.servers.map((server) => {
66
+ const id = typeof server.id === 'string' && server.id !== '' ? server.id : 'server'
67
+ const injectWorkspaceRoot = server.injectWorkspaceRoot !== undefined
68
+ ? server.injectWorkspaceRoot === true
69
+ : id === 'argo'
70
+ const serverPath = resolveMaybeRelative(server.serverPath)
71
+ ?? (id === 'argo' ? DEFAULT_SERVER_PATH : undefined)
72
+ const args = Array.isArray(server.args)
73
+ ? server.args.map(resolveMaybeRelative)
74
+ : (serverPath ? [serverPath] : [])
75
+ return withCommon({
76
+ id,
77
+ // mcp__<id>__ prefix mirrors the host's server-name convention.
78
+ prefix: typeof server.prefix === 'string' && server.prefix !== '' ? server.prefix : 'mcp__' + id + '__',
79
+ command: typeof server.command === 'string' && server.command !== '' ? server.command : 'node',
80
+ args,
81
+ env: server.env && typeof server.env === 'object' ? server.env : {},
82
+ cwd: server.cwd,
83
+ injectWorkspaceRoot,
84
+ })
85
+ })
86
+ }
87
+
88
+ // Legacy single-server shape: the argo workspace bridge.
89
+ const serverPath = config.serverPath ?? process.env.ARGO_SERVER_PATH ?? DEFAULT_SERVER_PATH
90
+ return [withCommon({
91
+ id: 'argo',
92
+ prefix: 'mcp__argo__',
93
+ command: 'node',
94
+ args: [serverPath],
95
+ env: {},
96
+ cwd: config.cwd,
97
+ injectWorkspaceRoot: true,
98
+ })]
99
+ }
100
+
101
+ /** Minimal MCP stdio client over one spawned server process. */
102
+ function createStdioClient(command, args, env, cwd) {
103
+ const child = spawn(command, args, {
104
+ stdio: ['pipe', 'pipe', 'pipe'],
105
+ env,
106
+ ...(cwd ? { cwd } : {}),
107
+ })
108
+ const pending = new Map()
109
+ let nextId = 1
110
+ readline.createInterface({ input: child.stdout }).on('line', (line) => {
111
+ let message
112
+ try { message = JSON.parse(line) } catch { return }
113
+ if (message && typeof message.id === 'number' && pending.has(message.id)) {
114
+ const { resolve, reject } = pending.get(message.id)
115
+ pending.delete(message.id)
116
+ if (message.error) reject(new Error(JSON.stringify(message.error)))
117
+ else resolve(message.result)
118
+ }
119
+ })
120
+ child.stderr.on('data', () => {}) // drain; the server logs to stderr
121
+ const request = (method, params) => new Promise((resolve, reject) => {
122
+ const id = nextId++
123
+ pending.set(id, { resolve, reject })
124
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n')
125
+ })
126
+ return {
127
+ request,
128
+ notify: (method, params) => {
129
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n')
130
+ },
131
+ close: () => child.kill(),
132
+ }
133
+ }
134
+
135
+ /** Connect one server, list its tools, and register them under its prefix. */
136
+ async function mountServer(ctx, spec, registered) {
137
+ const client = createStdioClient(spec.command, spec.args, spec.env, spec.cwd)
138
+ ctx.effect(() => () => client.close(), `dsh-argo-workspace.dispose(${spec.id})`)
139
+
140
+ let tools = []
141
+ try {
142
+ await client.request('initialize', {
143
+ protocolVersion: '2024-11-05',
144
+ capabilities: {},
145
+ clientInfo: { name: 'dsh-argo-workspace', version: '0.1.0' },
146
+ })
147
+ client.notify('notifications/initialized', {})
148
+ const listed = await client.request('tools/list', {})
149
+ tools = (listed && Array.isArray(listed.tools)) ? listed.tools : []
150
+ } catch (error) {
151
+ console.warn(`[dsh-argo-workspace] failed to connect to '${spec.id}' (${spec.args.join(' ')}): ${error && error.message ? error.message : error}`)
152
+ return
153
+ }
154
+
155
+ for (const tool of tools) {
156
+ const publicName = spec.prefix + tool.name
157
+ registered.push(ctx.tools.register({
158
+ name: publicName,
159
+ description: tool.description ?? '',
160
+ parameters: tool.inputSchema,
161
+ output: {
162
+ schema: {
163
+ type: 'object',
164
+ properties: {
165
+ content: { type: 'array', items: {} },
166
+ structuredContent: {},
167
+ },
168
+ required: ['content'],
169
+ additionalProperties: false,
170
+ },
171
+ render: (_args, value) => value.content,
172
+ },
173
+ execute: async (args, exec) => {
174
+ // SessionHeader.cwd (the durable session workspace), NOT
175
+ // requestHeader() — that returns the request EpochHeader
176
+ // (config/system/tools) which has no cwd field.
177
+ const injected = { ...(args && typeof args === 'object' ? args : {}) }
178
+ if (spec.injectWorkspaceRoot) {
179
+ const sessionCwd = exec && exec.agent && exec.agent.session
180
+ ? exec.agent.session.header?.cwd
181
+ : undefined
182
+ if (typeof sessionCwd === 'string' && sessionCwd !== '') {
183
+ injected.workspaceRoot = sessionCwd
184
+ // Broker-only trusted channel: the server allows WRITES to this
185
+ // workspace (see workspace-write-guard.js). Set AFTER the spread so
186
+ // a model-supplied value can never win.
187
+ injected.sessionWorkspaceRoot = sessionCwd
188
+ } else {
189
+ // No session workspace: never forward a model-supplied trusted field.
190
+ delete injected.sessionWorkspaceRoot
191
+ }
192
+ }
193
+ if (exec && exec.signal && exec.signal.aborted) {
194
+ throw new Error('aborted')
195
+ }
196
+ const result = await client.request('tools/call', {
197
+ name: tool.name,
198
+ arguments: injected,
199
+ })
200
+ const text = Array.isArray(result.content)
201
+ ? result.content
202
+ .map((block) => block && block.type === 'text' && typeof block.text === 'string'
203
+ ? block.text
204
+ : JSON.stringify(block))
205
+ .join('\n')
206
+ : (result.toolResult !== undefined ? JSON.stringify(result.toolResult) : '(no output)')
207
+ if (result.isError === true) throw new Error(text)
208
+ return {
209
+ content: [{ type: 'text', text }],
210
+ ...(result.structuredContent !== undefined ? { structuredContent: result.structuredContent } : {}),
211
+ }
212
+ },
213
+ }))
214
+ }
215
+ }
216
+
217
+ /** Connect every configured server, register its tools, keep clients until disposal. */
218
+ export async function apply(ctx, config) {
219
+ const specs = normalizeServerSpecs(config || {})
220
+ const registered = []
221
+ for (const spec of specs) {
222
+ await mountServer(ctx, spec, registered)
223
+ }
224
+ ctx.effect(() => () => {
225
+ for (const dispose of registered) dispose()
226
+ }, 'dsh-argo-workspace.tools')
227
+ }