@gt-fe/eap-runtime 0.0.13 → 0.0.15

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.
Files changed (86) hide show
  1. package/README.md +3 -1
  2. package/dist/api/handlers/invoke.handler.d.ts +1 -1
  3. package/dist/api/handlers/invoke.handler.js +3 -0
  4. package/dist/api/handlers/stream.handler.d.ts +1 -1
  5. package/dist/api/handlers/stream.handler.js +3 -0
  6. package/dist/common/index.d.ts +1 -0
  7. package/dist/common/index.js +1 -0
  8. package/dist/common/logger.d.ts +16 -4
  9. package/dist/common/logger.js +82 -16
  10. package/dist/common/runtime-package-version.d.ts +2 -0
  11. package/dist/common/runtime-package-version.js +5 -0
  12. package/dist/debug/llm-turn-log.d.ts +12 -0
  13. package/dist/debug/llm-turn-log.js +1 -0
  14. package/dist/graph/adapters/langgraph.adapter.d.ts +3 -1
  15. package/dist/graph/adapters/langgraph.adapter.js +66 -13
  16. package/dist/graph/adapters/subgraph-compiler.js +5 -2
  17. package/dist/graph/contracts/executable-graph.d.ts +13 -2
  18. package/dist/graph/nodes/llm-node.d.ts +3 -0
  19. package/dist/graph/nodes/llm-node.js +8 -0
  20. package/dist/graph/nodes/skill-bind-node.d.ts +3 -4
  21. package/dist/graph/nodes/skill-bind-node.js +1 -20
  22. package/dist/graph/nodes/tool-node.d.ts +2 -0
  23. package/dist/graph/nodes/tool-node.js +20 -14
  24. package/dist/memory/compression/index.d.ts +1 -1
  25. package/dist/memory/compression/index.js +1 -1
  26. package/dist/memory/compression/sliding-window.compression.js +7 -7
  27. package/dist/memory/index.d.ts +1 -1
  28. package/dist/memory/index.js +1 -1
  29. package/dist/memory/message-compression.service.d.ts +12 -1
  30. package/dist/memory/message-compression.service.js +29 -1
  31. package/dist/providers/http-llm-provider.d.ts +1 -0
  32. package/dist/providers/http-llm-provider.js +4 -1
  33. package/dist/providers/index.d.ts +1 -1
  34. package/dist/providers/index.js +1 -1
  35. package/dist/providers/local-llm-provider.d.ts +3 -0
  36. package/dist/providers/local-llm-provider.js +14 -2
  37. package/dist/providers/routing-llm-provider.js +12 -3
  38. package/dist/registry/deployment-snapshot.js +18 -16
  39. package/dist/registry/graph-loader.d.ts +10 -0
  40. package/dist/registry/graph-loader.js +115 -66
  41. package/dist/runtime/agent-runtime.d.ts +3 -3
  42. package/dist/runtime/agent-runtime.js +13 -3
  43. package/dist/runtime/dev-runtime.d.ts +2 -0
  44. package/dist/runtime/dev-runtime.js +2 -0
  45. package/dist/runtime/types.d.ts +2 -2
  46. package/dist/session/compression-middleware.d.ts +2 -1
  47. package/dist/session/compression-middleware.js +7 -4
  48. package/dist/sse/events/stats-events.d.ts +9 -0
  49. package/dist/token/compression-budget-integration.d.ts +1 -1
  50. package/dist/token/compression-budget-integration.js +18 -10
  51. package/dist/token/context-window.d.ts +42 -0
  52. package/dist/token/context-window.js +73 -0
  53. package/dist/token/index.d.ts +1 -0
  54. package/dist/token/index.js +1 -0
  55. package/dist/tool/dev-http-executor.d.ts +1 -0
  56. package/dist/tool/dev-http-executor.js +12 -0
  57. package/dist/tool/dev-mcp-executor.d.ts +11 -1
  58. package/dist/tool/dev-mcp-executor.js +26 -0
  59. package/dist/tool/dev-tool-executor.d.ts +2 -1
  60. package/dist/tool/dev-tool-executor.js +6 -0
  61. package/dist/tool/harness-command.js +1 -1
  62. package/dist/tool/harness-primitives.d.ts +7 -0
  63. package/dist/tool/harness-primitives.js +56 -5
  64. package/dist/tool/index.d.ts +2 -1
  65. package/dist/tool/index.js +1 -0
  66. package/dist/tool/local-mcp-client.d.ts +4 -1
  67. package/dist/tool/local-mcp-client.js +130 -3
  68. package/dist/tool/local-tool-executor.d.ts +2 -1
  69. package/dist/tool/local-tool-executor.js +15 -0
  70. package/dist/tool/mcp-list.d.ts +53 -0
  71. package/dist/tool/mcp-list.js +199 -0
  72. package/dist/tool/mcp-tool-executor.d.ts +6 -1
  73. package/dist/tool/mcp-tool-executor.js +52 -0
  74. package/dist/tool/skill-dir-infer.d.ts +17 -0
  75. package/dist/tool/skill-dir-infer.js +81 -0
  76. package/dist/tool/skill-script-refs.d.ts +1 -8
  77. package/dist/tool/skill-script-refs.js +32 -20
  78. package/dist/tool/system-prompt-builder.d.ts +35 -16
  79. package/dist/tool/system-prompt-builder.js +30 -58
  80. package/dist/tool/timeout.js +4 -1
  81. package/dist/tool/tool-execution-router.d.ts +2 -1
  82. package/dist/tool/tool-execution-router.js +7 -0
  83. package/dist/tool/tool-executor.interface.d.ts +11 -0
  84. package/package.json +6 -33
  85. package/dist/registry/mcp-stdio-cwd.d.ts +0 -5
  86. package/dist/registry/mcp-stdio-cwd.js +0 -13
@@ -0,0 +1,73 @@
1
+ /**
2
+ * 本轮 prompt 占用的上下文窗:used = 本轮 inputTokens,window = ModelRecord.capabilities.maxContextTokens。
3
+ * 不是账单合计,也不是 max_tokens。
4
+ * parts 按本轮 system / messages / tools 字符比例把 used 拆开(无独立 tokenizer)。
5
+ */
6
+ export function contextWindowUsage(used, window) {
7
+ return buildContextUsage(used, window);
8
+ }
9
+ export function splitContextUsage(used, sections) {
10
+ if (!(used > 0))
11
+ return undefined;
12
+ const systemChars = Math.max(0, sections.systemChars);
13
+ const messageChars = Math.max(0, sections.messageChars);
14
+ const toolChars = Math.max(0, sections.toolChars);
15
+ const total = systemChars + messageChars + toolChars;
16
+ if (total <= 0)
17
+ return undefined;
18
+ const system = Math.floor((used * systemChars) / total);
19
+ const tools = Math.floor((used * toolChars) / total);
20
+ const messages = used - system - tools;
21
+ return { system, messages, tools };
22
+ }
23
+ export function turnSectionChars(config, messages) {
24
+ const systemChars = (config.systemPrompt ?? '').length;
25
+ let messageChars = 0;
26
+ for (const message of messages) {
27
+ if (message.content)
28
+ messageChars += message.content.length;
29
+ if (message.toolCalls)
30
+ messageChars += jsonChars(message.toolCalls);
31
+ }
32
+ const toolChars = config.tools?.length ? jsonChars(config.tools) : 0;
33
+ return { systemChars, messageChars, toolChars };
34
+ }
35
+ export function buildContextUsage(used, window, sections) {
36
+ const safeUsed = typeof used === 'number' && Number.isFinite(used) && used >= 0 ? Math.floor(used) : 0;
37
+ const win = typeof window === 'number' && Number.isFinite(window) && window > 0
38
+ ? Math.floor(window)
39
+ : undefined;
40
+ const parts = sections ? splitContextUsage(safeUsed, sections) : undefined;
41
+ if (safeUsed === 0 && win == null && !parts)
42
+ return undefined;
43
+ return {
44
+ used: safeUsed,
45
+ ...(win != null ? { window: win } : {}),
46
+ ...(parts ? { parts } : {}),
47
+ };
48
+ }
49
+ export function pickContextFields(raw) {
50
+ const out = {};
51
+ if (typeof raw.used === 'number' && Number.isFinite(raw.used))
52
+ out.used = raw.used;
53
+ if (typeof raw.window === 'number' && Number.isFinite(raw.window) && raw.window > 0) {
54
+ out.window = raw.window;
55
+ }
56
+ if (raw.parts && typeof raw.parts === 'object' && !Array.isArray(raw.parts)) {
57
+ const p = raw.parts;
58
+ if (typeof p.system === 'number'
59
+ && typeof p.messages === 'number'
60
+ && typeof p.tools === 'number') {
61
+ out.parts = { system: p.system, messages: p.messages, tools: p.tools };
62
+ }
63
+ }
64
+ return out;
65
+ }
66
+ function jsonChars(value) {
67
+ try {
68
+ return JSON.stringify(value).length;
69
+ }
70
+ catch {
71
+ return 0;
72
+ }
73
+ }
@@ -7,6 +7,7 @@ export { SessionBudgetChecker, SessionBudgetExhaustedError, } from './session-bu
7
7
  export type { NodeTokenUsage, TokenBreakdown, TaskTokenSummary, } from './token-stats.service.js';
8
8
  export { TokenStatsService } from './token-stats.service.js';
9
9
  export type { TokenDetailMode } from './token-detail-mode.js';
10
+ export { contextWindowUsage, buildContextUsage, splitContextUsage, turnSectionChars, pickContextFields, type ContextWindowUsage, type ContextUsageParts, } from './context-window.js';
10
11
  export { TokenStatsEmitter } from './token-detail-mode.js';
11
12
  export { ContextWindowBudget, createContextBudget, type ContextWindowBudgetConfig, type ContextSource, type SourceAllocation, type BudgetAllocation, DEFAULT_CONTEXT_BUDGET_CONFIG, } from './context-window-budget.js';
12
13
  export { CharBasedTokenEstimator, ModelBasedTokenEstimator, createTokenEstimator, type ITokenEstimator, } from './token-estimator.js';
@@ -3,6 +3,7 @@ export { PostgresTokenBudgetRepository } from './pg-token-budget.repository.js';
3
3
  export { TokenBudgetGuard, TokenBudgetExhaustedError, } from './token-budget-guard.js';
4
4
  export { SessionBudgetChecker, SessionBudgetExhaustedError, } from './session-budget-checker.js';
5
5
  export { TokenStatsService } from './token-stats.service.js';
6
+ export { contextWindowUsage, buildContextUsage, splitContextUsage, turnSectionChars, pickContextFields, } from './context-window.js';
6
7
  export { TokenStatsEmitter } from './token-detail-mode.js';
7
8
  // 上下文窗口预算分配
8
9
  export { ContextWindowBudget, createContextBudget, DEFAULT_CONTEXT_BUDGET_CONFIG, } from './context-window-budget.js';
@@ -9,6 +9,7 @@ export interface DevHttpExecution {
9
9
  baseUrl?: string;
10
10
  pathTemplate?: string;
11
11
  allowedHosts?: string[];
12
+ header?: Record<string, string>;
12
13
  timeout?: number;
13
14
  }
14
15
  export declare function resolveHttpExecutionFromRequest(request: ToolCallRequest): DevHttpExecution | null;
@@ -23,9 +23,20 @@ export function resolveHttpExecutionFromRequest(request) {
23
23
  allowedHosts: Array.isArray(e.allowedHosts)
24
24
  ? e.allowedHosts.filter((item) => typeof item === 'string')
25
25
  : [],
26
+ header: readHeaderMap(e.header),
26
27
  timeout: resolveToolTimeoutSec(e.timeout, DEFAULT_HTTP_TOOL_TIMEOUT_SEC),
27
28
  };
28
29
  }
30
+ function readHeaderMap(raw) {
31
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
32
+ return undefined;
33
+ const out = {};
34
+ for (const [key, value] of Object.entries(raw)) {
35
+ if (typeof value === 'string')
36
+ out[key] = value;
37
+ }
38
+ return Object.keys(out).length > 0 ? out : undefined;
39
+ }
29
40
  export function buildHttpToolUrl(spec, args) {
30
41
  const url = buildSharedHttpToolUrl(spec.baseUrl ?? '', spec.pathTemplate ?? '', args);
31
42
  assertHttpHostAllowlisted(url, spec.allowedHosts ?? []);
@@ -54,6 +65,7 @@ export class DevHttpExecutor {
54
65
  const timer = setTimeout(() => controller.abort(), toToolTimeoutMs(timeoutSec, DEFAULT_HTTP_TOOL_TIMEOUT_SEC));
55
66
  const headers = {
56
67
  Accept: 'application/json, text/plain;q=0.9',
68
+ ...(spec.header ?? {}),
57
69
  };
58
70
  const init = { method, headers, signal: controller.signal };
59
71
  if (method !== 'GET' && method !== 'HEAD') {
@@ -17,13 +17,23 @@
17
17
  * @see docs/workflows/local-agent-dev-loop.md
18
18
  * @see runtime/src/tool/local-mcp-client.ts
19
19
  */
20
+ import { type McpRuntimeExecution } from '@gt-fe/eap-contracts';
20
21
  import { type ILocalMcpClient } from './local-mcp-client.js';
21
- import type { IToolExecutor, ToolCallRequest, ToolCallResult } from './tool-executor.interface.js';
22
+ import type { IToolExecutor, McpListedTool, ToolCallRequest, ToolCallResult } from './tool-executor.interface.js';
22
23
  export interface DevMcpExecutorOptions {
23
24
  client?: ILocalMcpClient;
24
25
  }
25
26
  export declare class DevMcpExecutor implements IToolExecutor {
26
27
  private readonly client;
28
+ private readonly listCache;
27
29
  constructor(options?: DevMcpExecutorOptions);
28
30
  execute(request: ToolCallRequest): Promise<ToolCallResult>;
31
+ listTools(request: ToolCallRequest): Promise<McpListedTool[]>;
29
32
  }
33
+ /**
34
+ * 把 tool.manifest.execution(McpExecution 结构,transport 可为 stdio/sse/streamable_http)
35
+ * 规范化为 LocalMcpClient 消费的 McpRuntimeExecution(transport 只支持 stdio/streamable_http)。
36
+ *
37
+ * sse 归并到 streamable_http(LocalMcpClient 的 http 分支已能解析 SSE 响应)。
38
+ */
39
+ export declare function normalizeMcpExecution(raw: unknown, fallbackToolName: string): McpRuntimeExecution | null;
@@ -22,6 +22,7 @@ import { isAbortError } from './timeout.js';
22
22
  import { LocalMcpClient } from './local-mcp-client.js';
23
23
  export class DevMcpExecutor {
24
24
  client;
25
+ listCache = new Map();
25
26
  constructor(options = {}) {
26
27
  this.client = options.client ?? new LocalMcpClient();
27
28
  }
@@ -64,6 +65,28 @@ export class DevMcpExecutor {
64
65
  };
65
66
  }
66
67
  }
68
+ async listTools(request) {
69
+ const execution = normalizeExecution(request.execution, request.toolName);
70
+ if (!execution) {
71
+ throw new Error(`Dev MCP: tool "${request.toolName}" has no valid mcp execution config (expect manifest.execution.protocol="mcp")`);
72
+ }
73
+ const url = execution.server?.url ?? '';
74
+ const cmd = execution.server?.command ?? '';
75
+ const args = (execution.server?.args ?? []).join('\0');
76
+ const key = `${url || `stdio:${cmd}:${args}`}\0${request.authContext?.userId ?? ''}\0${request.authContext?.agentCode ?? ''}`;
77
+ const cached = this.listCache.get(key);
78
+ if (cached)
79
+ return cached;
80
+ const pending = this.client.list(execution, { userId: request.authContext?.userId });
81
+ this.listCache.set(key, pending);
82
+ try {
83
+ return await pending;
84
+ }
85
+ catch (err) {
86
+ this.listCache.delete(key);
87
+ throw err;
88
+ }
89
+ }
67
90
  }
68
91
  /**
69
92
  * 把 tool.manifest.execution(McpExecution 结构,transport 可为 stdio/sse/streamable_http)
@@ -71,6 +94,9 @@ export class DevMcpExecutor {
71
94
  *
72
95
  * sse 归并到 streamable_http(LocalMcpClient 的 http 分支已能解析 SSE 响应)。
73
96
  */
97
+ export function normalizeMcpExecution(raw, fallbackToolName) {
98
+ return normalizeExecution(raw, fallbackToolName);
99
+ }
74
100
  function normalizeExecution(raw, fallbackToolName) {
75
101
  if (!raw || typeof raw !== 'object')
76
102
  return null;
@@ -10,7 +10,7 @@
10
10
  * - 其余 → NOT_CONFIGURED
11
11
  */
12
12
  import { DevMcpExecutor } from './dev-mcp-executor.js';
13
- import type { IToolExecutor, ToolCallRequest, ToolCallResult } from './tool-executor.interface.js';
13
+ import type { IToolExecutor, McpListedTool, ToolCallRequest, ToolCallResult } from './tool-executor.interface.js';
14
14
  export interface DevToolExecutorOptions {
15
15
  mcp?: ConstructorParameters<typeof DevMcpExecutor>[0];
16
16
  }
@@ -20,4 +20,5 @@ export declare class DevToolExecutor implements IToolExecutor {
20
20
  private readonly local;
21
21
  constructor(options?: DevToolExecutorOptions);
22
22
  execute(request: ToolCallRequest): Promise<ToolCallResult>;
23
+ listTools(request: ToolCallRequest): Promise<McpListedTool[]>;
23
24
  }
@@ -47,6 +47,12 @@ export class DevToolExecutor {
47
47
  runtime: 'local',
48
48
  };
49
49
  }
50
+ async listTools(request) {
51
+ if (hasMcpExecution(request.execution)) {
52
+ return this.mcp.listTools(request);
53
+ }
54
+ return [];
55
+ }
50
56
  }
51
57
  function hasMcpExecution(raw) {
52
58
  if (!raw || typeof raw !== 'object')
@@ -21,7 +21,7 @@ export function commandDialectLines(platform = process.platform) {
21
21
  else {
22
22
  lines.push('基础命令:当前目录 `pwd`;列目录 `ls`;环境变量 `$VAR`;串命令用 `&&`。');
23
23
  }
24
- lines.push('解释器:`python`/`python3`、`node`/`nodejs`、`sh`/`bash`(Windows 上 `sh` 需本机有 Git Bash 等)。', '可执行文件:用绝对路径或相对于 cwd(Windows `.exe`/`.bat`/`.cmd`;Unix 二进制需可执行或用 `sh`)。', '`execute_command` 的 cwd 是用户目录。技能包只读。包内任意文件用 `EAP_SKILL_DIR` 加相对路径(不限 `scripts/`)。读到的 SKILL.md 里像脚本的相对路径已展开为绝对路径。不要把 cwd 设成技能目录。相对写出在用户目录。');
24
+ lines.push('解释器:`python`/`python3`、`node`/`nodejs`、`sh`/`bash`(Windows 上 `sh` 需本机有 Git Bash 等)。', '可执行文件:用绝对路径或相对于 cwd(Windows `.exe`/`.bat`/`.cmd`;Unix 二进制需可执行或用 `sh`)。', '`execute_command` 的 cwd 是用户目录。技能包只读。包内任意文件用 `EAP_SKILL_DIR` 加相对路径(不限 `scripts/`)。读到的 SKILL.md 里本包存在的相对路径已展开为绝对路径。不要把 cwd 设成技能目录。相对写出在用户目录。');
25
25
  return lines;
26
26
  }
27
27
  export function resolveShellSpawn(command, platform = process.platform) {
@@ -34,6 +34,13 @@ export declare function resolveJailFromState(state: {
34
34
  export declare function resolveJailRoots(options?: HarnessJailOptions): string[];
35
35
  /** 路径落在写根外的 extraRoot 时,返回该 extraRoot。 */
36
36
  export declare function extraRootContaining(abs: string, extraRoots: string[], workdir: string): string | undefined;
37
+ /**
38
+ * 相对 read_file:`技能名/文件`、全库唯一命中、显式 skillDir,否则用户目录。
39
+ */
40
+ export declare function locateSkillRelativeRead(filePath: string, workdir: string | undefined, skillDir: string | undefined, extraRoots?: string[]): {
41
+ path: string;
42
+ relativeBase: string | undefined;
43
+ };
37
44
  export declare function resolveJailedPath(requested: string, roots: string[], opts?: {
38
45
  mustExist?: boolean;
39
46
  relativeBase?: string;
@@ -8,7 +8,7 @@
8
8
  * 开发态可选 overlay:`EAP_LOCAL_SKILLS_DIR` / `EAP_LOCAL_TOOLS_DIR`(只读)。
9
9
  */
10
10
  import { spawn } from 'node:child_process';
11
- import { constants as fsConstants, realpathSync } from 'node:fs';
11
+ import { constants as fsConstants, existsSync, realpathSync } from 'node:fs';
12
12
  import { mkdir, open, readFile } from 'node:fs/promises';
13
13
  import path from 'node:path';
14
14
  import { BUILTIN_TOOL_SOURCE, DEFAULT_BUILTIN_TIMEOUT_MS, MAX_BUILTIN_TIMEOUT_MS, clampBuiltinTimeout, isBuiltinToolExecution, resolveBuiltinToolsConfig, } from '@gt-fe/eap-contracts';
@@ -252,6 +252,53 @@ function resolveViaExistingAncestor(abs) {
252
252
  cur = parent;
253
253
  }
254
254
  }
255
+ function userReadBase(workdir) {
256
+ return workdir?.trim()
257
+ ? (tryRealpath(path.resolve(workdir)) ?? path.resolve(workdir))
258
+ : undefined;
259
+ }
260
+ function skillPackageRoots(roots) {
261
+ const out = [];
262
+ const seen = new Set();
263
+ for (const root of roots) {
264
+ const resolved = path.resolve(root);
265
+ if (seen.has(resolved))
266
+ continue;
267
+ seen.add(resolved);
268
+ if (existsSync(path.join(resolved, 'SKILL.md')) || existsSync(path.join(resolved, 'skill.md'))) {
269
+ out.push(resolved);
270
+ }
271
+ }
272
+ return out;
273
+ }
274
+ /**
275
+ * 相对 read_file:`技能名/文件`、全库唯一命中、显式 skillDir,否则用户目录。
276
+ */
277
+ export function locateSkillRelativeRead(filePath, workdir, skillDir, extraRoots) {
278
+ const trimmed = filePath.trim();
279
+ const userBase = userReadBase(workdir);
280
+ if (!trimmed || path.isAbsolute(trimmed))
281
+ return { path: trimmed, relativeBase: userBase };
282
+ const packages = skillPackageRoots([
283
+ ...(skillDir?.trim() ? [skillDir] : []),
284
+ ...(extraRoots ?? []),
285
+ ]);
286
+ const slash = trimmed.replace(/\\/g, '/');
287
+ const segs = slash.split('/').filter(Boolean);
288
+ if (segs.length >= 2) {
289
+ const hit = packages.find((pkg) => path.basename(pkg) === segs[0]);
290
+ if (hit)
291
+ return { path: segs.slice(1).join('/'), relativeBase: hit };
292
+ }
293
+ const matches = packages.filter((pkg) => existsSync(path.resolve(pkg, trimmed)));
294
+ if (matches.length === 1)
295
+ return { path: trimmed, relativeBase: matches[0] };
296
+ const skillBase = skillDir?.trim() ? path.resolve(skillDir) : undefined;
297
+ if (skillBase && existsSync(path.resolve(skillBase, trimmed))) {
298
+ return { path: trimmed, relativeBase: skillBase };
299
+ }
300
+ return { path: trimmed, relativeBase: userBase };
301
+ }
255
302
  export function resolveJailedPath(requested, roots, opts) {
256
303
  const trimmed = requested.trim();
257
304
  if (!trimmed || trimmed.includes('\0')) {
@@ -344,10 +391,10 @@ export class HarnessPrimitiveExecutor {
344
391
  async readFile(request) {
345
392
  const filePath = String(request.arguments.path ?? request.arguments.file ?? '');
346
393
  const workdir = this.workdirFor(request);
347
- const relativeBase = workdir ? (tryRealpath(workdir) ?? path.resolve(workdir)) : undefined;
348
- const target = resolveJailedPath(filePath, this.rootsFor(request), {
394
+ const located = locateSkillRelativeRead(filePath, workdir, request.skillDir, this.extraRootsFor(request));
395
+ const target = resolveJailedPath(located.path, this.rootsFor(request), {
349
396
  mustExist: true,
350
- relativeBase,
397
+ relativeBase: located.relativeBase,
351
398
  });
352
399
  const buf = await readFile(target);
353
400
  const config = builtinConfig();
@@ -481,7 +528,7 @@ export class HarnessPrimitiveExecutor {
481
528
  }
482
529
  export function withHarnessPrimitives(inner, options) {
483
530
  const harness = new HarnessPrimitiveExecutor(options);
484
- return {
531
+ const wrapped = {
485
532
  execute(request) {
486
533
  if (isBuiltinToolExecution(request.execution)) {
487
534
  return harness.execute(request);
@@ -489,4 +536,8 @@ export function withHarnessPrimitives(inner, options) {
489
536
  return inner.execute(request);
490
537
  },
491
538
  };
539
+ if (typeof inner.listTools === 'function') {
540
+ wrapped.listTools = (request) => inner.listTools(request);
541
+ }
542
+ return wrapped;
492
543
  }
@@ -1,4 +1,5 @@
1
- export type { AuthContext, ToolCallRequest, ToolCallResult, LLMToolCall, LLMToolCallResponse, IToolExecutor, } from './tool-executor.interface.js';
1
+ export type { AuthContext, ToolCallRequest, ToolCallResult, LLMToolCall, LLMToolCallResponse, IToolExecutor, McpListedTool, } from './tool-executor.interface.js';
2
+ export { hydrateMcpTools, isMcpDiscoverStub, needsMcpHydration, parseListedTools, } from './mcp-list.js';
2
3
  export { toLoadedToolMap, type LoadedToolDefinition, type LoadedToolMap, type LoadedToolManifestRef, } from './loaded-tool-definition.js';
3
4
  export { ToolOrchestrator } from './tool-orchestrator.js';
4
5
  export { HttpToolExecutor, type HttpToolExecutorOptions, } from './http-tool-executor.js';
@@ -1,3 +1,4 @@
1
+ export { hydrateMcpTools, isMcpDiscoverStub, needsMcpHydration, parseListedTools, } from './mcp-list.js';
1
2
  export { toLoadedToolMap, } from './loaded-tool-definition.js';
2
3
  export { ToolOrchestrator } from './tool-orchestrator.js';
3
4
  export { HttpToolExecutor, } from './http-tool-executor.js';
@@ -2,10 +2,11 @@
2
2
  * LocalMcpClient — 桌面本地 MCP 调用(stdio / streamable_http)
3
3
  *
4
4
  * mcp-data-service 等上游要求 HTTP 头 `userId`(或 query),再注入 `params._meta.userId`。
5
- * 身份优先来自 Runtime ExecutionContext(Token.sub);manifest headers.userId 作兜底。
5
+ * 身份优先来自 Runtime ExecutionContext(Token.sub);manifest header.userId 作兜底。
6
6
  */
7
7
  import type { McpRuntimeExecution } from '@gt-fe/eap-contracts';
8
8
  import { type FileArtifact } from './file-artifact.js';
9
+ import type { McpListedTool } from './tool-executor.interface.js';
9
10
  export interface LocalMcpInvokeContext {
10
11
  /** Platform Token.sub / ExecutionContext.userId */
11
12
  userId?: string | null;
@@ -16,8 +17,10 @@ export interface LocalMcpInvokeResult {
16
17
  }
17
18
  export interface ILocalMcpClient {
18
19
  invoke(execution: McpRuntimeExecution, args: Record<string, unknown>, context?: LocalMcpInvokeContext): Promise<LocalMcpInvokeResult>;
20
+ list(execution: McpRuntimeExecution, context?: LocalMcpInvokeContext): Promise<McpListedTool[]>;
19
21
  }
20
22
  export declare function formatMcpOutput(result: unknown): string;
21
23
  export declare class LocalMcpClient implements ILocalMcpClient {
22
24
  invoke(execution: McpRuntimeExecution, args: Record<string, unknown>, context?: LocalMcpInvokeContext): Promise<LocalMcpInvokeResult>;
25
+ list(execution: McpRuntimeExecution, context?: LocalMcpInvokeContext): Promise<McpListedTool[]>;
23
26
  }
@@ -2,12 +2,13 @@
2
2
  * LocalMcpClient — 桌面本地 MCP 调用(stdio / streamable_http)
3
3
  *
4
4
  * mcp-data-service 等上游要求 HTTP 头 `userId`(或 query),再注入 `params._meta.userId`。
5
- * 身份优先来自 Runtime ExecutionContext(Token.sub);manifest headers.userId 作兜底。
5
+ * 身份优先来自 Runtime ExecutionContext(Token.sub);manifest header.userId 作兜底。
6
6
  */
7
7
  import { spawn } from 'node:child_process';
8
8
  import readline from 'node:readline';
9
9
  import { isAbortError, resolveToolTimeoutSec, toToolTimeoutMs, toolTimeoutErrorMessage, } from './timeout.js';
10
10
  import { fileArtifactFromToolOutput, fileBasesFromEnv } from './file-artifact.js';
11
+ import { nextListCursor, parseListedTools } from './mcp-list.js';
11
12
  export function formatMcpOutput(result) {
12
13
  const r = result;
13
14
  if (r?.structuredContent !== undefined) {
@@ -53,7 +54,7 @@ function expandEnvRefs(value) {
53
54
  return value.replace(/\$\{([A-Z][A-Z0-9_]*)\}/g, (_match, name) => process.env[name] ?? '');
54
55
  }
55
56
  function resolveUserId(_execution, context) {
56
- // 仅用调用期身份(Token/ExecutionContext);禁止依赖 manifest 静态 headers.userId
57
+ // 仅用调用期身份(Token/ExecutionContext);禁止依赖 manifest 静态 header.userId
57
58
  const fromCtx = context?.userId?.trim();
58
59
  return fromCtx || undefined;
59
60
  }
@@ -142,6 +143,61 @@ async function invokeStdio(execution, args, context) {
142
143
  rl.close();
143
144
  }
144
145
  }
146
+ async function listStdio(execution, context) {
147
+ const { server } = execution;
148
+ const command = server.command ?? 'node';
149
+ const spawnArgs = server.args ?? [];
150
+ const timeoutMs = toToolTimeoutMs(execution.timeout);
151
+ const userId = resolveUserId(execution, context);
152
+ const child = spawn(command, spawnArgs, {
153
+ stdio: ['pipe', 'pipe', 'pipe'],
154
+ cwd: server.cwd,
155
+ env: {
156
+ ...process.env,
157
+ ...server.env,
158
+ ...(userId ? { EAP_MCP_USER_ID: userId } : {}),
159
+ },
160
+ });
161
+ const session = {
162
+ process: child,
163
+ nextId: 1,
164
+ pending: new Map(),
165
+ timeoutMs,
166
+ };
167
+ const rl = readline.createInterface({ input: child.stdout });
168
+ rl.on('line', (line) => {
169
+ if (!line.trim())
170
+ return;
171
+ try {
172
+ const msg = JSON.parse(line);
173
+ if (msg.id == null)
174
+ return;
175
+ const p = session.pending.get(msg.id);
176
+ if (!p)
177
+ return;
178
+ session.pending.delete(msg.id);
179
+ if (msg.error)
180
+ p.reject(new Error(msg.error.message));
181
+ else
182
+ p.resolve(msg.result);
183
+ }
184
+ catch {
185
+ /* ignore */
186
+ }
187
+ });
188
+ try {
189
+ await rpcStdio(session, 'initialize', {
190
+ protocolVersion: '2024-11-05',
191
+ capabilities: {},
192
+ clientInfo: { name: 'eap-runtime-local', version: '0.1.0' },
193
+ });
194
+ return await collectListedTools((params) => rpcStdio(session, 'tools/list', params));
195
+ }
196
+ finally {
197
+ child.kill();
198
+ rl.close();
199
+ }
200
+ }
145
201
  async function postMcp(url, headers, payload, signal) {
146
202
  const response = await fetch(url, {
147
203
  method: 'POST',
@@ -171,7 +227,7 @@ async function invokeStreamableHttp(execution, args, context) {
171
227
  const controller = new AbortController();
172
228
  const timer = setTimeout(() => controller.abort(), timeoutMs);
173
229
  const userId = resolveUserId(execution, context);
174
- const rawHeaders = execution.server.headers ?? {};
230
+ const rawHeaders = execution.server.header ?? {};
175
231
  const expandedHeaders = {};
176
232
  for (const [key, value] of Object.entries(rawHeaders)) {
177
233
  expandedHeaders[key] = expandEnvRefs(value);
@@ -219,6 +275,61 @@ async function invokeStreamableHttp(execution, args, context) {
219
275
  clearTimeout(timer);
220
276
  }
221
277
  }
278
+ async function listStreamableHttp(execution, context) {
279
+ const url = expandEnvRefs(execution.server.url);
280
+ if (!url) {
281
+ throw new Error('streamable_http MCP requires server.url');
282
+ }
283
+ const timeoutSec = resolveToolTimeoutSec(execution.timeout);
284
+ const timeoutMs = toToolTimeoutMs(timeoutSec);
285
+ const controller = new AbortController();
286
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
287
+ const userId = resolveUserId(execution, context);
288
+ const rawHeaders = execution.server.header ?? {};
289
+ const expandedHeaders = {};
290
+ for (const [key, value] of Object.entries(rawHeaders)) {
291
+ expandedHeaders[key] = expandEnvRefs(value);
292
+ }
293
+ const headers = {
294
+ 'Content-Type': 'application/json',
295
+ Accept: 'application/json, text/event-stream',
296
+ ...expandedHeaders,
297
+ };
298
+ if (userId) {
299
+ headers.userId = userId;
300
+ }
301
+ try {
302
+ const init = await postMcp(url, headers, {
303
+ jsonrpc: '2.0',
304
+ id: 1,
305
+ method: 'initialize',
306
+ params: {
307
+ protocolVersion: '2024-11-05',
308
+ capabilities: {},
309
+ clientInfo: { name: 'eap-runtime-local', version: '0.1.0' },
310
+ },
311
+ }, controller.signal);
312
+ const listHeaders = { ...headers };
313
+ if (init.sessionId)
314
+ listHeaders['mcp-session-id'] = init.sessionId;
315
+ let nextId = 2;
316
+ return await collectListedTools(async (params) => {
317
+ const listed = await postMcp(url, listHeaders, { jsonrpc: '2.0', id: nextId++, method: 'tools/list', params }, controller.signal);
318
+ return listed.body.result;
319
+ });
320
+ }
321
+ catch (err) {
322
+ if (isAbortError(err)) {
323
+ const timeoutErr = new Error(toolTimeoutErrorMessage(timeoutSec));
324
+ timeoutErr.name = 'TimeoutError';
325
+ throw timeoutErr;
326
+ }
327
+ throw err instanceof Error ? err : new Error(String(err));
328
+ }
329
+ finally {
330
+ clearTimeout(timer);
331
+ }
332
+ }
222
333
  function parseMcpHttpBody(raw) {
223
334
  const trimmed = raw.trim();
224
335
  if (trimmed.startsWith('{')) {
@@ -244,6 +355,22 @@ export class LocalMcpClient {
244
355
  }
245
356
  return invokeStdio(execution, args, context);
246
357
  }
358
+ async list(execution, context) {
359
+ if (execution.transport === 'streamable_http') {
360
+ return listStreamableHttp(execution, context);
361
+ }
362
+ return listStdio(execution, context);
363
+ }
364
+ }
365
+ async function collectListedTools(rpcList) {
366
+ const tools = [];
367
+ let cursor;
368
+ do {
369
+ const result = await rpcList(cursor ? { cursor } : {});
370
+ tools.push(...parseListedTools(result));
371
+ cursor = nextListCursor(result);
372
+ } while (cursor);
373
+ return tools;
247
374
  }
248
375
  function toInvokeResult(raw, serverEnv) {
249
376
  const structured = (raw && typeof raw === 'object' && !Array.isArray(raw))
@@ -5,7 +5,7 @@
5
5
  * 或 request.execution 带可解析的 code bundlePath+handler,则直接本机执行。
6
6
  */
7
7
  import type { ILocalToolInstallStore } from '@gt-fe/eap-contracts';
8
- import type { IToolExecutor, ToolCallRequest, ToolCallResult } from './tool-executor.interface.js';
8
+ import type { IToolExecutor, McpListedTool, ToolCallRequest, ToolCallResult } from './tool-executor.interface.js';
9
9
  import { type ILocalMcpClient } from './local-mcp-client.js';
10
10
  export interface LocalToolExecutorOptions {
11
11
  installStore: ILocalToolInstallStore;
@@ -30,6 +30,7 @@ export declare class LocalToolExecutor implements IToolExecutor {
30
30
  private readonly mcpClient;
31
31
  constructor(options: LocalToolExecutorOptions);
32
32
  execute(request: ToolCallRequest): Promise<ToolCallResult>;
33
+ listTools(request: ToolCallRequest): Promise<McpListedTool[]>;
33
34
  private executeCodeSpec;
34
35
  private executeCode;
35
36
  private executeMcp;
@@ -11,6 +11,7 @@ import { ErrorCodes, resolveReadyInstallByToolName, resolveMcpExecutionFromInsta
11
11
  import { isAbortError, toToolTimeoutMs, toolTimeoutErrorMessage } from './timeout.js';
12
12
  import { fileArtifactFromToolOutput, fileBasesFromEnv } from './file-artifact.js';
13
13
  import { LocalMcpClient } from './local-mcp-client.js';
14
+ import { normalizeMcpExecution } from './dev-mcp-executor.js';
14
15
  /**
15
16
  * 根据 execution.runtime 解析执行命令:
16
17
  * - node(默认)→ process.execPath + [handlerPath]
@@ -171,6 +172,20 @@ export class LocalToolExecutor {
171
172
  runtime: 'local',
172
173
  };
173
174
  }
175
+ async listTools(request) {
176
+ const fromRequest = normalizeMcpExecution(request.execution, request.toolName);
177
+ if (fromRequest) {
178
+ return this.mcpClient.list(fromRequest, { userId: request.authContext?.userId });
179
+ }
180
+ const install = resolveReadyInstallByToolName(this.options.installStore, request.toolName);
181
+ if (install && install.installStatus === 'ready' && (install.type ?? request.toolType) === 'mcp') {
182
+ const execution = resolveMcpExecutionFromInstall(install);
183
+ if (execution) {
184
+ return this.mcpClient.list(execution, { userId: request.authContext?.userId });
185
+ }
186
+ }
187
+ throw new Error(`MCP tools/list: no mcp execution for "${request.toolName}"`);
188
+ }
174
189
  async executeCodeSpec(request, spec) {
175
190
  try {
176
191
  const stdout = await runHandler(spec.handlerPath, request.arguments, spec.timeoutMs ?? this.options.timeoutMs ?? 30_000, { runtime: spec.runtime, args: spec.args });