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
2026-09-09 10:33:18 +07:00
parent 626450eb06
commit 2587e03beb
9 changed files with 236 additions and 42 deletions
+18 -9
View File
@@ -355,6 +355,10 @@ const session = new Session({
approveSubagent = session.approveForSubagent();
recordSubagent = (u) => session.recordSubagentUsage(u);
if (mcp) {
const { bindMcpGuard } = await import('./mcp');
bindMcpGuard(mcp, () => plugins, () => process.cwd());
}
async function shutdown(code: number): Promise<never> {
clearTimeout(saveTimer);
@@ -371,7 +375,7 @@ if (printArg !== undefined) {
}
if (!yolo) {
process.stderr.write(
'shiro: headless denies write_file, edit_file, multi_edit, bash and mcp tools unless --yolo is passed\n',
'shiro: headless denies write_file, edit_file, multi_edit, bash and mcp_call unless --yolo is passed\n',
);
}
const code = await runHeadless({ session, prompt, format: has('--json') ? 'json' : 'text' });
@@ -473,22 +477,27 @@ const hooks: AppHooks = {
const servers = Object.entries(cfg.mcpServers ?? {});
if (servers.length === 0) return 'no MCP servers configured\n\n`/mcp add` sets one up.';
const live = new Map<string, number>();
const liveDirect = new Map<string, number>();
for (const name of Object.keys(mcp?.tools ?? {})) {
if (name === '__mcpServerNames') continue;
const server = /^mcp__([^_]+(?:_[^_]+)*)__/.exec(name)?.[1];
if (server) live.set(server, (live.get(server) ?? 0) + 1);
if (server) liveDirect.set(server, (liveDirect.get(server) ?? 0) + 1);
}
const hasMeta = !!(mcp?.tools as Record<string, unknown>)?.['mcp_list'];
const failed = new Map((mcp?.errors ?? []).map((e) => [e.server, e.message]));
const rows = servers.map(([name, config]) => {
const where = 'url' in config ? config.url : [config.command, ...(config.args ?? [])].join(' ');
const isDirect = (config as { expose?: string }).expose === 'direct';
const state = failed.has(name)
? `failed: ${failed.get(name)}`
: live.has(name)
? `${live.get(name)} tools`
: has('--no-mcp')
? 'not connected (--no-mcp)'
: 'not connected this session';
: isDirect
? (liveDirect.has(name) ? `${liveDirect.get(name)} tools (direct)` : 'not connected')
: hasMeta
? 'via mcp_list/mcp_inspect/mcp_call (no schema until called)'
: has('--no-mcp')
? 'not connected (--no-mcp)'
: 'not connected this session';
return `- \`${name}\` (${'url' in config ? 'remote' : 'local'}) - ${state}\n ${where}`;
});
@@ -639,7 +648,7 @@ const facts: HeaderFact[] = [
memory && memory.all().length > 0
? { label: 'memory', value: `${memory.all().length} notes about this project` }
: undefined,
mcp && Object.keys(mcp.tools).length > 0 ? { label: 'mcp', value: `${Object.keys(mcp.tools).length} tools` } : undefined,
mcp && Object.keys(mcp.tools).filter((k) => k !== '__mcpServerNames').length > 0 ? { label: 'mcp', value: `${Object.keys(mcp.tools).filter((k) => k !== '__mcpServerNames').length} tools${(mcp.tools as Record<string, unknown>)['mcp_list'] ? ' (mcp_list/mcp_inspect/mcp_call)' : ''}` } : undefined,
!mcp && cfg.mcpServers && Object.keys(cfg.mcpServers).length > 0
? { label: 'mcp', value: `${Object.keys(cfg.mcpServers).length} configured, not connected (--no-mcp)`, tone: 'warn' as const }
: undefined,