mcp tool server for claude code — native tool execution via stdio protocol

Replaces prompt injection workaround with a proper MCP server that dynamically
discovers marketplace tools and exposes them as callable tools to Claude Code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 13:21:50 +00:00
co-authored by Claude Opus 4.6
parent ecd83dc1ec
commit 86ddcbfead
5 changed files with 417 additions and 34 deletions
+32 -26
View File
@@ -1,18 +1,18 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { join, resolve } from 'node:path';
import type { Subprocess } from 'bun';
import type { PiEvent, MessageCost } from '../../api/pi/types';
import type { ClaudeSpawnParams, ClaudeSpawnStreamingParams, ClaudeCodeResult } from '../protocol';
import { getState, setClaudeSession, clearClaudeSession, getClaudeSession } from './state';
import { setClaudeSession, clearClaudeSession, getClaudeSession } from './state';
import { getProxySecret } from './proxy';
import { discoverTools } from '../../tool-registry';
import type { ToolDefinition } from '../../tool-registry';
import { DATA_PATH, getHomeDir } from '../../data-path';
import { DATA_PATH, getHomeDir, getGlobalToolsDir, getUserToolsDir } from '../../data-path';
import { generateContainerContext } from '../../generate-container-context';
const SEND_TIMEOUT_MS = 5 * 60 * 1000;
const PROXY_PORT = process.env.ANTHROPIC_PROXY_PORT ?? '5051';
const MCP_SERVER_SCRIPT = resolve(import.meta.dir, '../../mcp-tool-server.ts');
function refreshClaudeMd(email: string): void {
const claudeDir = join(getHomeDir(email), '.claude');
if (!existsSync(claudeDir)) mkdirSync(claudeDir, { recursive: true });
@@ -20,25 +20,29 @@ function refreshClaudeMd(email: string): void {
writeFileSync(join(claudeDir, 'CLAUDE.md'), readFileSync(contextFile, 'utf-8'));
}
function buildToolContext(email: string): string {
const tools = discoverTools(email);
if (tools.length === 0) return '';
function generateMcpConfig(email: string): string {
const contextDir = join(DATA_PATH, email, '.container-context');
mkdirSync(contextDir, { recursive: true });
const configPath = join(contextDir, 'mcp.json');
const sections = tools.map((tool) => {
const lines = [`## ${tool.name}`, tool.description];
const inputEntries = Object.entries(tool.inputs);
if (inputEntries.length > 0) {
const inputParts = inputEntries.map(([name, input]) => {
const opt = input.optional ? ', optional' : '';
const vals = input.values ? `, values: ${input.values}` : '';
return `${name} (${input.type}${vals}${opt}): ${input.description}`;
});
lines.push(`Inputs: ${inputParts.join('; ')}`);
}
return lines.join('\n');
});
const toolsDirs = [getGlobalToolsDir(), getUserToolsDir(email)].filter(existsSync).join(':');
return `The following tools are available on this platform:\n\n${sections.join('\n\n---\n\n')}\n\n---\n\n`;
const config = {
mcpServers: {
'officer-tools': {
type: 'stdio',
command: 'bun',
args: ['run', MCP_SERVER_SCRIPT],
env: {
PI_TOOLS_DIRS: toolsDirs,
OFFICER_EMAIL_DB: join(DATA_PATH, email, 'emails.db'),
},
},
},
};
writeFileSync(configPath, JSON.stringify(config));
return configPath;
}
const toShellUsername = (username: string, email: string): string => {
@@ -97,12 +101,12 @@ export async function spawnClaude(params: ClaudeSpawnParams): Promise<ClaudeCode
const shellUsername = toShellUsername(username, email);
refreshClaudeMd(email);
const mcpConfigPath = generateMcpConfig(email);
const existingSession = getClaudeSession(sessionKey);
const isResume = !!existingSession;
const effectivePrompt = isResume ? prompt : buildToolContext(email) + prompt;
const claudeArgs = [CLAUDE_BIN, '-p', effectivePrompt, '--dangerously-skip-permissions', '--output-format', 'json'];
const claudeArgs = [CLAUDE_BIN, '-p', prompt, '--dangerously-skip-permissions', '--output-format', 'json', '--mcp-config', mcpConfigPath];
const subModel = params.model?.split('/')[1];
if (subModel) claudeArgs.push('--model', subModel);
@@ -194,20 +198,22 @@ export async function spawnClaudeStreaming(
const workDir = cwd ?? homeDir;
refreshClaudeMd(email);
const mcpConfigPath = generateMcpConfig(email);
const existingSession = getClaudeSession(sessionKey);
const isResume = !!existingSession;
const effectivePrompt = isResume ? prompt : buildToolContext(email) + prompt;
const claudeArgs = [
CLAUDE_BIN,
'-p',
effectivePrompt,
prompt,
'--dangerously-skip-permissions',
'--output-format',
'stream-json',
'--verbose',
'--include-partial-messages',
'--mcp-config',
mcpConfigPath,
];
const subModel = params.model?.split('/')[1];