index.ts•6.57 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
ListToolsRequestSchema,
CallToolRequestSchema,
TextContent,
} from "@modelcontextprotocol/sdk/types.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { spawnSync } from "child_process";
import { readFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
interface OracleConfig {
model?: string;
models?: string[];
reasoning?: string;
command?: string;
}
interface OracleMcpConfig {
oracle?: OracleConfig;
oracles?: OracleConfig[];
}
const DEFAULT_ORACLE_CONFIG: OracleConfig = {
models: ["gpt-5.1-codex-mini"],
reasoning: "medium",
command: "codex",
};
/**
* Normalizes an OracleConfig to always have a models array.
* Supports backward compatibility with single model field.
*/
function normalizeModels(config: OracleConfig): string[] {
if (config.models && config.models.length > 0) {
return config.models;
}
if (config.model) {
return [config.model];
}
return [];
}
function loadConfig(): OracleMcpConfig {
const configPath = join(homedir(), ".oracle-mcp.json");
try {
const content = readFileSync(configPath, "utf-8");
return JSON.parse(content) as OracleMcpConfig;
} catch {
return {};
}
}
/**
* Constructs command-line arguments for invoking the oracle CLI.
* Returns arguments as an array to avoid shell escaping issues.
*/
export function buildOracleArgs(
command: string,
model: string,
reasoning: string | undefined,
prompt: string
): string[] {
if (command === "codex" || command.includes("codex")) {
// Codex: codex exec --model <model> [-c reasoning_level=<level>] "<prompt>"
const args = ["exec", "--model", model];
if (reasoning) {
args.push("-c", `reasoning_level=${reasoning}`);
}
args.push(prompt);
return args;
} else if (command === "gemini" || command.includes("gemini")) {
// Gemini: gemini -p --model <model> "<prompt>"
return ["-p", "--model", model, prompt];
} else {
// Claude: claude -p --model <model> "<prompt>"
return ["-p", "--model", model, prompt];
}
}
/**
* Invokes the oracle CLI and returns the response.
* Extracted for testability.
*/
export function invokeOracle(
command: string,
args: string[]
): { stdout: string; status: number; error?: Error } {
const result = spawnSync(command, args, { encoding: "utf-8" });
return {
stdout: result.stdout,
status: result.status ?? 1,
error: result.error,
};
}
const config = loadConfig();
// Support both single oracle and multiple oracles (with fallback)
let oracleConfigs: OracleConfig[];
if (config.oracles && config.oracles.length > 0) {
oracleConfigs = config.oracles;
} else if (config.oracle) {
oracleConfigs = [config.oracle];
} else {
oracleConfigs = [DEFAULT_ORACLE_CONFIG];
}
// Primary oracle for descriptions
const oracleConfig: OracleConfig = oracleConfigs[0];
const server = new Server({
name: "oracle-mcp",
version: "0.1.0",
});
server.registerCapabilities({
tools: {},
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
const models = normalizeModels(oracleConfig);
const modelStr = models.length === 1 ? models[0] : `${models.length} models`;
const baseDescription = `Consult the oracle (${modelStr}) via ${oracleConfig.command || "oracle"} CLI`;
const reasoningPart = oracleConfig.reasoning
? ` with ${oracleConfig.reasoning}-level reasoning`
: "";
const capabilityPart = `. The oracle provides expert reasoning and analysis for complex problem-solving.`;
const usagePart =
" Use when: (1) planning complex tasks with multiple tradeoffs, (2) you are <=90% confident in your approach, (3) you need analysis of architectural decisions or design patterns.";
const fullDescription = baseDescription + reasoningPart + capabilityPart + usagePart;
return {
tools: [
{
name: "consult_oracle",
description: fullDescription,
inputSchema: {
type: "object",
properties: {
prompt: {
type: "string",
description: "The question or problem to consult the oracle about. Be specific about context, constraints, and what decision or analysis you need.",
},
},
required: ["prompt"],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const name = request.params.name;
const args = request.params.arguments || {};
if (name === "consult_oracle") {
const prompt = (args as Record<string, unknown>).prompt as string;
const errors: string[] = [];
// Try each oracle in sequence, and within each oracle try each model (fallback)
for (let oracleIdx = 0; oracleIdx < oracleConfigs.length; oracleIdx++) {
const cfg = oracleConfigs[oracleIdx];
const models = normalizeModels(cfg);
for (let modelIdx = 0; modelIdx < models.length; modelIdx++) {
const model = models[modelIdx];
try {
const cmdName = cfg.command || "oracle";
const spawnArgs = buildOracleArgs(
cmdName,
model,
cfg.reasoning,
prompt
);
const result = invokeOracle(cmdName, spawnArgs);
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`Command failed with status ${result.status}`);
}
return {
content: [
{
type: "text",
text: result.stdout,
} as TextContent,
],
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
errors.push(
`Oracle ${oracleIdx + 1}, model ${modelIdx + 1} (${model}): ${errorMsg}`
);
}
}
}
// All oracles and models failed, return accumulated errors
return {
content: [
{
type: "text",
text: `All oracles failed:\n${errors.join("\n")}`,
} as TextContent,
],
isError: true,
};
}
return {
content: [
{
type: "text",
text: `Unknown tool: ${name}`,
} as TextContent,
],
isError: true,
};
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Oracle MCP server running on stdio");
}
main().catch(console.error);