Skip to main content
Glama
index.ts6.61 kB
#!/usr/bin/env node /** * Councly MCP Server * * Model Context Protocol server for Councly council hearings. * Communicates via stdio with MCP clients (Claude Code, Codex, etc.). * * Usage: * COUNCLY_API_KEY=cnc_xxx npx @councly/mcp * * Or in Claude Code settings: * { * "mcpServers": { * "councly": { * "command": "npx", * "args": ["@councly/mcp"], * "env": { * "COUNCLY_API_KEY": "cnc_xxx" * } * } * } * } */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { CounclyClient, CounclyApiError } from './api-client.js'; import { TOOL_DEFINITIONS, counclyHearingSchema, counclyStatusSchema } from './tools.js'; // Server metadata const SERVER_NAME = 'councly-mcp'; const SERVER_VERSION = '0.1.0'; // Get API key from environment function getApiKey(): string { const apiKey = process.env.COUNCLY_API_KEY; if (!apiKey) { console.error('Error: COUNCLY_API_KEY environment variable is required'); console.error('Get your API key at https://councly.ai/settings/mcp'); process.exit(1); } return apiKey; } // Format hearing result for display function formatHearingResult(status: { hearingId: string; status: string; verdict?: string | null; trustScore?: number | null; totalCost?: number; counselSummaries?: Array<{ seat: string; model: string; lastTurn: string; }>; progress?: number; error?: string; }): string { const lines: string[] = []; lines.push(`## Hearing ${status.hearingId}`); lines.push(`**Status:** ${status.status}`); if (status.status === 'completed' || status.status === 'early_stopped') { if (status.verdict) { lines.push(''); lines.push('### Verdict'); lines.push(status.verdict); } if (status.trustScore !== null && status.trustScore !== undefined) { lines.push(''); lines.push(`**Trust Score:** ${status.trustScore}/100`); } if (status.counselSummaries && status.counselSummaries.length > 0) { lines.push(''); lines.push('### Counsel Perspectives'); for (const counsel of status.counselSummaries) { lines.push(`**${counsel.seat}** (${counsel.model}):`); lines.push(counsel.lastTurn); lines.push(''); } } if (status.totalCost !== undefined) { lines.push(`**Cost:** ${status.totalCost} credits`); } } else if (status.status === 'failed') { lines.push(`**Error:** ${status.error || 'Unknown error'}`); } else { // In progress if (status.progress !== undefined) { lines.push(`**Progress:** ${status.progress}%`); } } return lines.join('\n'); } async function main() { const apiKey = getApiKey(); const client = new CounclyClient({ apiKey }); // Create MCP server const server = new Server( { name: SERVER_NAME, version: SERVER_VERSION, }, { capabilities: { tools: {}, }, } ); // Handle list tools request server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: TOOL_DEFINITIONS, }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'councly_hearing': { const parsed = counclyHearingSchema.parse(args); // Create the hearing const hearing = await client.createHearing({ subject: parsed.subject, preset: parsed.preset, workflow: parsed.workflow, }); // If not waiting, return immediately if (!parsed.wait) { return { content: [ { type: 'text', text: `Hearing created: ${hearing.hearingId}\nStatus: ${hearing.status}\nPreset: ${hearing.preset}\nCost: ${hearing.cost.credits} credits\n\nUse councly_status to check progress.`, }, ], }; } // Wait for completion const status = await client.waitForCompletion(hearing.hearingId, { timeoutMs: parsed.timeout_seconds * 1000, onProgress: (s) => { // Progress updates could be logged if needed if (s.progress !== undefined) { process.stderr.write(`\rProgress: ${s.progress}%`); } }, }); return { content: [ { type: 'text', text: formatHearingResult(status), }, ], }; } case 'councly_status': { const parsed = counclyStatusSchema.parse(args); const status = await client.getHearingStatus(parsed.hearing_id); return { content: [ { type: 'text', text: formatHearingResult(status), }, ], }; } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { if (error instanceof CounclyApiError) { return { content: [ { type: 'text', text: `API Error (${error.code}): ${error.message}`, }, ], isError: true, }; } if (error instanceof McpError) { throw error; } // Zod validation errors if (error instanceof Error && error.name === 'ZodError') { return { content: [ { type: 'text', text: `Validation Error: ${error.message}`, }, ], isError: true, }; } // Unknown error const message = error instanceof Error ? error.message : 'Unknown error'; return { content: [ { type: 'text', text: `Error: ${message}`, }, ], isError: true, }; } }); // Connect via stdio const transport = new StdioServerTransport(); await server.connect(transport); // Log startup to stderr (stdout is for MCP protocol) console.error(`${SERVER_NAME} v${SERVER_VERSION} started`); } main().catch((error) => { console.error('Fatal error:', error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/slmnsrf/councly-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server