TODO Next: MCP without the schema tax (phi meta-tools)

- mcpExpose=phi|direct|auto on RawConfig + MCPConfig; phi default keeps
  direct as a measured option for small servers (2-tool cheaper direct)
- src/mcp.ts: buildMcpMetaTools + phi branch in connectMcp: lazy
  list/inspect/call behind 3 fixed schemas instead of N per-tool schemas;
  direct branch kept; bindMcpGuard wires permission+PluginHost guard for
  MCP calls through the same gate as built-ins
- prompt mcpServers names-only under phi; under direct schemas travel
  as before — 20-tool server ~2750 tok -> ~phi (names) until mcp_call
- session: isDirect mcpServerNames non-enumerable marker, activeTools
  hides mcp_call from pre-wired rules when inside mcp_call, /tools
  shows mcp_call and Enabled/Withheld reflects the 3 meta-names under phi
- permission: mcp_* as read (free), mcp_call mutating keyed by
  server.tool, same top-level suppression + ask-to-approve as other
  mutating tools
- cli: bindMcpGuard + /mcp list shows exposure + mcp_* listed;
  headless denies line mentions mcp_list
- commit.ts: git_commit_message kept in git set via toolSetOf/
  disabledToolNames overlay

Tests: 798 pass, 0 fail; tsc exit 0; mcp.test.ts covers phi stays
empty until mcp_call and direct still namespaced.
This commit is contained in:
asepharyana committed 2026-09-09 10:33:18 +07:00
1 parent 626450eb06
commit 2587e03beb
9 files changed
+236 -42

No files matched your search

+19 -1
View File
@@ -337,7 +337,8 @@ export class Session {
*/
activeTools(): string[] {
const withheld = new Set(disabledToolNames(this.opts.toolSets));
const all = Object.keys(this.tools).filter((name) => !withheld.has(name));
// __mcpServerNames is bookkeeping, not a tool
const all = Object.keys(this.tools).filter((name) => name !== '__mcpServerNames' && !withheld.has(name));
if (!this.variant.allowTools) return all;
return all.filter((name) => this.variant.allowTools!.includes(name));
}
@@ -399,6 +400,22 @@ export class Session {
return { usd, ceiling, overWarn: usd >= ceiling * 0.8, overLimit: usd >= ceiling };
}
private mcpServerNamesForPrompt(): string[] | undefined {
const hasMeta = this.tools['mcp_list'] !== undefined;
if (!hasMeta) return undefined;
const marker = (this.tools as Record<string, unknown>)['__mcpServerNames'];
if (Array.isArray(marker) && marker.length > 0) return [...marker].sort() as string[];
// fallback: derive from direct if marker missing (tests that inject tools manually)
const direct = Object.keys(this.tools).filter((n) => n.startsWith('mcp__'));
if (direct.length === 0) return undefined;
const names = new Set<string>();
for (const n of direct) {
const m = /^mcp__([^_]+(?:_[^_]+)*)__/.exec(n);
if (m) names.add(m[1]!);
}
return names.size > 0 ? [...names].sort() : undefined;
}
private systemFor(): string {
const mem = this.opts.memory;
const memoryBlock = mem ? mem.render() : '';
@@ -412,6 +429,7 @@ export class Session {
plugins: this.pluginHost?.appendix ?? '',
availableTools: this.activeTools(),
canAsk: this.opts.ask !== undefined && this.activeTools().includes('ask'),
...(this.mcpServerNamesForPrompt() ? { mcpServers: this.mcpServerNamesForPrompt() } : {}),
});
}