/**
* Server Core
*
* Main MCP server setup and configuration for the AskMeMCP server.
* Handles tool and prompt registration, and request routing.
*
* Capabilities:
* - Tools: Interactive human-in-the-loop tools (ask-one-question, ask-multiple-choice, challenge-hypothesis, choose-next)
* - Prompts: Template-based prompts for human guidance (human-decision, expert-consultation, creative-brainstorm)
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema,
McpError,
ErrorCode
} from '@modelcontextprotocol/sdk/types.js';
import {
ALL_TOOLS,
handleAskOneQuestion,
handleAskMultipleChoice,
handleChallengeHypothesis,
handleChooseNext,
type AskOneQuestionArgs,
type AskMultipleChoiceArgs,
type ChallengeHypothesisArgs,
type ChooseNextArgs
} from '../tools/index.js';
import {
ALL_PROMPTS,
PROMPT_GENERATORS,
type PromptName
} from '../prompts/index.js';
import { RequestManager } from './request-manager.js';
import { notifyBrowser } from './browser-bridge.js';
/**
* MCP Server configuration
*/
export interface ServerConfig {
name: string;
version: string;
description: string;
}
/**
* Create and configure the MCP server
*/
export function createMCPServer(config: ServerConfig, requestManager: RequestManager, exitAfterCommand: boolean = false): Server {
// Helper function to handle exit after command
const handleExitAfterCommand = () => {
if (exitAfterCommand) {
if (process.env.ASK_ME_MCP_DEBUG) {
console.error('[ask-me-mcp] Exit after command flag set, shutting down...');
}
// Use setTimeout to allow the response to be sent before exiting
setTimeout(() => {
process.exit(0);
}, 100);
}
};
const server = new Server(
{
name: config.name,
version: config.version,
description: config.description,
},
{
capabilities: {
tools: {
call: true,
},
prompts: {
listChanged: true,
},
},
}
);
// Register the list tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
const result = {
tools: ALL_TOOLS.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
};
handleExitAfterCommand();
return result;
});
// Register the list prompts handler
server.setRequestHandler(ListPromptsRequestSchema, async () => {
const result = {
prompts: ALL_PROMPTS.map(prompt => ({
name: prompt.name,
description: prompt.description,
arguments: prompt.arguments,
})),
};
handleExitAfterCommand();
return result;
});
// Register the get prompt handler
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const promptName = request.params.name as PromptName;
const args = request.params.arguments as Record<string, unknown>;
const generator = PROMPT_GENERATORS[promptName];
if (!generator) {
throw new McpError(ErrorCode.InvalidRequest, `Unknown prompt: ${promptName}`);
}
try {
const result = await generator(args as any);
handleExitAfterCommand();
return result;
} catch (error) {
if (process.env.ASK_ME_MCP_DEBUG) {
console.error(`[ask-me-mcp] Error generating prompt ${promptName}:`, error);
}
throw new McpError(
ErrorCode.InternalError,
`Failed to generate prompt: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
});
// Register the tool call handler
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
const toolName = request.params.name;
const args = request.params.arguments as Record<string, unknown>;
let result;
switch (toolName) {
case 'ask-one-question':
result = await handleAskOneQuestion(
args as unknown as AskOneQuestionArgs,
requestManager.getStorage(),
notifyBrowser,
extra
);
break;
case 'ask-multiple-choice':
result = await handleAskMultipleChoice(
args as unknown as AskMultipleChoiceArgs,
requestManager.getStorage(),
notifyBrowser,
extra
);
break;
case 'challenge-hypothesis':
result = await handleChallengeHypothesis(
args as unknown as ChallengeHypothesisArgs,
requestManager.getStorage(),
notifyBrowser,
extra
);
break;
case 'choose-next':
result = await handleChooseNext(
args as unknown as ChooseNextArgs,
requestManager.getStorage(),
notifyBrowser,
extra
);
break;
default:
throw new McpError(ErrorCode.InvalidRequest, `Unknown tool: ${toolName}`);
}
// Exit after command if flag is set (debug mode)
handleExitAfterCommand();
return result;
});
return server;
}
/**
* Start the MCP server with stdio transport
*/
export async function startMCPServer(server: Server): Promise<void> {
// Only log if debug mode is enabled
if (process.env.ASK_ME_MCP_DEBUG) {
console.error('[ask-me-mcp] Starting stdio MCP server...');
console.error('[ask-me-mcp] Process ID:', process.pid);
console.error('[ask-me-mcp] Node version:', process.version);
console.error('[ask-me-mcp] Available capabilities: tools, prompts');
}
const transport = new StdioServerTransport();
// Add transport event handlers for debugging
transport.onclose = () => {
if (process.env.ASK_ME_MCP_DEBUG) {
console.error('[ask-me-mcp] Transport closed');
}
};
transport.onerror = (error) => {
if (process.env.ASK_ME_MCP_DEBUG) {
console.error('[ask-me-mcp] Transport error:', error);
}
};
await server.connect(transport);
if (process.env.ASK_ME_MCP_DEBUG) {
console.error('[ask-me-mcp] Server ready for connections');
}
}
/**
* Setup graceful shutdown handlers
*/
export function setupGracefulShutdown(server: Server, httpServer: any): void {
const shutdown = async (signal: string) => {
if (process.env.ASK_ME_MCP_DEBUG) {
console.error(`[ask-me-mcp] Received ${signal}, shutting down...`);
}
httpServer.close();
await server.close();
process.exit(0);
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
// Keep the process alive
process.stdin.resume();
}