Skip to main content
Glama
index.ts21.7 kB
#!/usr/bin/env node import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { z } from 'zod' import { readFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import crypto from 'node:crypto' import YAML from 'yaml' import { CoderSwapClient } from './coderswap-client.js' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const PROMPT_RELATIVE_PATH = '../mcp_starter_prompt.yaml' const PROMPT_HASH = '48fc8e470dfbadf5e83dbd5713758a28e263806a8132b99a4dc048fa16d29419' const GUARDRAIL_KEYWORDS = [ 'reset guardrails', 'disable guardrails', 'ignore guardrails', 'bypass guardrails', 'disable safety', 'bypass safety', 'disable protections', 'turn off protections' ] function formatValue(value: any, indent = ' '): string { if (value === null || value === undefined) { return `${indent}- (none)` } if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return `${indent}- ${String(value)}` } if (Array.isArray(value)) { if (value.length === 0) { return `${indent}- (none)` } return value .map(item => typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean' ? `${indent}- ${String(item)}` : formatValue(item, `${indent} `) ) .join('\n') } if (typeof value === 'object') { const entries = Object.entries(value) if (entries.length === 0) { return `${indent}- (empty)` } return entries .map(([key, val]) => `${indent}- ${key}:\n${formatValue(val, `${indent} `)}`) .join('\n') } return `${indent}- ${String(value)}` } function renderPrompt(data: any): string { const sections: string[] = [] const version = data?.version ?? 'unknown' const updated = data?.last_updated ?? 'unknown' sections.push( `CoderSwap MCP System Guardrails\nVersion: ${version} (Last updated ${updated})` ) if (Array.isArray(data?.owners) && data.owners.length > 0) { const owners = data.owners .map((owner: any) => { const name = owner?.name ?? 'Unknown' const contact = owner?.contact ? ` (${owner.contact})` : '' return ` - ${name}${contact}` }) .join('\n') sections.push(`Owners:\n${owners}`) } if (data?.description) { sections.push(`Description:\n ${String(data.description).trim()}`) } function pushSection(title: string, value: any) { if (value === undefined || value === null) return sections.push(`== ${title.toUpperCase()} ==\n${formatValue(value)}`) } pushSection('Product Overview', data?.product_overview) pushSection('Identity', data?.identity) pushSection('Privacy', data?.privacy) pushSection('Validation and Security', data?.validation_and_security) pushSection('Evaluation Standards', data?.evaluation_standards) pushSection('Promotion Policy', data?.promotion_policy) pushSection('Workflow', data?.workflow) pushSection('Communication Style', data?.communication_style) pushSection('Available Tools', data?.available_tools) pushSection('Human in Loop', data?.human_in_loop) pushSection('Error Handling', data?.error_handling) pushSection('Session Management', data?.session_management) pushSection('Workflow Templates', data?.workflow_templates) return sections.join('\n\n') } function containsGuardrailBypass(payload: unknown): boolean { if (payload === null || payload === undefined) return false if (typeof payload === 'string') { const lower = payload.toLowerCase() return GUARDRAIL_KEYWORDS.some(keyword => lower.includes(keyword)) } if (Array.isArray(payload)) { return payload.some(item => containsGuardrailBypass(item)) } if (typeof payload === 'object') { return Object.values(payload).some(value => containsGuardrailBypass(value)) } return false } function guardrailViolationResponse() { return { content: [{ type: 'text' as const, text: '✗ Request rejected: system guardrails cannot be bypassed.' }], isError: true } } // Environment configuration const baseUrl = process.env.CODERSWAP_BASE_URL || 'http://localhost:8000' const apiKey = process.env.CODERSWAP_API_KEY const DEBUG = process.env.DEBUG === 'true' // Logging utility function log(level: 'info' | 'error' | 'debug', message: string, data?: any) { if (level === 'debug' && !DEBUG) return const timestamp = new Date().toISOString() const logData = data ? ` ${JSON.stringify(data)}` : '' console.error(`[${timestamp}] [${level.toUpperCase()}] ${message}${logData}`) } // Load guardrail system prompt let systemPrompt: string try { const promptPath = path.resolve(__dirname, PROMPT_RELATIVE_PATH) const rawPrompt = readFileSync(promptPath, 'utf-8') const hash = crypto.createHash('sha256').update(rawPrompt, 'utf8').digest('hex') if (hash !== PROMPT_HASH) { console.error( `ERROR: Guardrail prompt hash mismatch. Expected ${PROMPT_HASH}, received ${hash}. Refusing to start.` ) process.exit(1) } const parsedPrompt = YAML.parse(rawPrompt) if (!parsedPrompt || typeof parsedPrompt !== 'object') { throw new Error('Parsed prompt is empty or invalid.') } systemPrompt = renderPrompt(parsedPrompt) if (!systemPrompt || !systemPrompt.trim()) { throw new Error('Rendered system prompt is empty.') } log('info', 'Guardrail prompt loaded successfully', { prompt_hash: hash }) } catch (error) { console.error( 'ERROR: Failed to load guardrail prompt.', error instanceof Error ? error.message : error ) process.exit(1) } // Validate API key if (!apiKey) { console.error('ERROR: CODERSWAP_API_KEY environment variable is required') process.exit(1) } // Initialize CoderSwap API client const client = new CoderSwapClient({ baseUrl, apiKey }) log('info', `CoderSwap MCP Server starting with backend: ${baseUrl}`) // Initialize MCP Server const server = new McpServer({ name: 'coderswap-mcp', version: '0.1.0', systemPrompt }) // Tool 0: Create Project server.registerTool( 'coderswap_create_project', { title: 'Create CoderSwap Project', description: 'Create a new vector search project in CoderSwap', inputSchema: { name: z.string().min(1, 'Project name is required'), description: z.string().optional() }, outputSchema: { project_id: z.string(), name: z.string(), status: z.string().optional() } }, async ({ name, description }) => { try { log('debug', 'Creating project', { name, description }) const project: any = await client.createProject({ name, description }) const output = { project_id: project.project_id, name: project.name || name, status: project.status } log('info', `Created project: ${project.project_id}`) return { content: [{ type: 'text', text: `✓ Created project "${name}" (ID: ${project.project_id})` }], structuredContent: output } } catch (error) { log('error', 'Failed to create project', { error: error instanceof Error ? error.message : error }) return { content: [{ type: 'text', text: `✗ Failed to create project: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true } } } ) // Tool 1: List Projects server.registerTool( 'coderswap_list_projects', { title: 'List CoderSwap Projects', description: 'List all CoderSwap projects available to your API key', inputSchema: {}, outputSchema: { count: z.number(), projects: z.array(z.object({ project_id: z.string(), name: z.string().optional(), doc_count: z.number().optional() })) } }, async () => { try { log('debug', 'Listing projects') const projects = await client.listProjects() const output = { count: projects.length, projects: projects.map(p => ({ project_id: p.project_id, name: p.name, doc_count: p.doc_count, search_mode: (p as any).search_mode })) } if (projects.length === 0) { return { content: [{ type: 'text', text: 'No projects found' }], structuredContent: output } } const projectList = projects .map(p => { const lines = [ `• ${p.name || 'Untitled Project'}`, ` ID: ${p.project_id}`, ` Docs: ${p.doc_count ?? 0}` ] if ((p as any).search_mode) { lines.push(` Search Mode: ${(p as any).search_mode}`) } return lines.join('\n') }) .join('\n\n') log('info', `Found ${projects.length} projects`) return { content: [{ type: 'text', text: `Found ${projects.length} project(s):\n\n${projectList}` }], structuredContent: output } } catch (error) { log('error', 'Failed to list projects', { error: error instanceof Error ? error.message : error }) return { content: [{ type: 'text', text: `✗ Failed to list projects: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true } } } ) // Tool 2: Get Project Stats server.registerTool( 'coderswap_get_project_stats', { title: 'Get CoderSwap Project Stats', description: 'Get statistics and information about a specific project', inputSchema: { project_id: z.string().min(1, 'project_id is required') }, outputSchema: { project_id: z.string(), name: z.string().optional(), doc_count: z.number().optional(), created_at: z.string().optional() } }, async ({ project_id }) => { try { log('debug', 'Getting project stats', { project_id }) const stats = await client.getProjectStats(project_id) const output = { project_id: stats.project_id, name: stats.name, doc_count: stats.doc_count, created_at: stats.created_at } log('info', `Retrieved stats for project: ${project_id}`) return { content: [{ type: 'text', text: `Project: ${stats.name || project_id}\nDocuments: ${stats.doc_count || 0}\nCreated: ${stats.created_at || 'Unknown'}` }], structuredContent: output } } catch (error) { log('error', 'Failed to get project stats', { project_id, error: error instanceof Error ? error.message : error }) return { content: [{ type: 'text', text: `✗ Failed to get project stats: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true } } } ) // Tool 3: Research Ingest server.registerTool( 'coderswap_research_ingest', { title: 'CoderSwap Research Ingest', description: 'Submit research summary and URLs for web crawling, chunking, embedding, and optional DSL generation', inputSchema: { project_id: z.string().min(1, 'project_id is required'), research_summary: z.string().optional(), urls: z.array(z.string().url()).min(1, 'At least one URL is required'), intent: z.string().optional(), depth: z.number().min(0).max(1).default(0), generate_dsl: z.boolean().default(true) }, outputSchema: { job_id: z.string(), project_id: z.string(), status: z.string() } }, async ({ project_id, research_summary, urls, intent, depth = 0, generate_dsl = true }) => { if (containsGuardrailBypass({ project_id, research_summary, urls, intent })) { return guardrailViolationResponse() } try { log('debug', 'Starting research ingest', { project_id, url_count: urls.length, generate_dsl }) const job: any = await client.researchIngest({ project_id, research_summary, urls, intent, depth, generate_dsl }) const output = { job_id: job.job_id, project_id, status: 'queued' } log('info', `Queued research ingest job: ${job.job_id}`) return { content: [{ type: 'text', text: `✓ Queued research ingest job: ${job.job_id}\n\nCrawling ${urls.length} URL(s)...\nDSL Generation: ${generate_dsl ? 'enabled' : 'disabled'}\n\nUse coderswap_get_job_status to monitor progress.` }], structuredContent: output } } catch (error) { log('error', 'Failed to start research ingest', { project_id, error: error instanceof Error ? error.message : error }) return { content: [{ type: 'text', text: `✗ Failed to start research ingest: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true } } } ) // Tool 4: Get Job Status server.registerTool( 'coderswap_get_job_status', { title: 'Get CoderSwap Job Status', description: 'Check the status of a research ingestion job', inputSchema: { job_id: z.string().min(1, 'job_id is required') }, outputSchema: { job_id: z.string(), state: z.string(), crawled_count: z.number().optional(), failed_count: z.number().optional() } }, async ({ job_id }) => { try { log('debug', 'Checking job status', { job_id }) const job = await client.getJobStatus(job_id) const output = { job_id: job.job_id, state: job.state, crawled_count: job.crawled_count, failed_count: job.failed_count } log('info', `Job ${job_id} status: ${job.state}`) let statusText = `Job: ${job_id}\nStatus: ${job.state}` if (job.crawled_count !== undefined) { statusText += `\nCrawled: ${job.crawled_count} documents` } if (job.failed_count !== undefined && job.failed_count > 0) { statusText += `\nFailed: ${job.failed_count} documents` } return { content: [{ type: 'text', text: statusText }], structuredContent: output } } catch (error) { log('error', 'Failed to get job status', { job_id, error: error instanceof Error ? error.message : error }) return { content: [{ type: 'text', text: `✗ Failed to get job status: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true } } } ) // Tool 5: Search server.registerTool( 'coderswap_search', { title: 'CoderSwap Hybrid Search', description: 'Execute a hybrid search query against a CoderSwap project using DSL-powered ranking', inputSchema: { project_id: z.string().min(1, 'project_id is required'), query: z.string().min(1, 'query is required'), top_k: z.number().min(1).max(50).default(10), snippet_length: z.number().min(50).max(1000).default(200) }, outputSchema: { query: z.string(), result_count: z.number(), results: z.array(z.object({ score: z.number(), title: z.string().optional(), snippet: z.string().optional() })) } }, async ({ project_id, query, top_k = 10, snippet_length = 200 }) => { try { log('debug', 'Executing search', { project_id, query, top_k }) const result = await client.search({ project_id, query, top_k, snippet_length }) const output = { query, result_count: result.results.length, results: result.results.slice(0, top_k).map(r => ({ score: r.score, title: r.title, snippet: r.snippet })) } if (result.results.length === 0) { return { content: [{ type: 'text', text: `No results found for: "${query}"` }], structuredContent: output } } // Format results with rich detail const formattedResults = result.results .slice(0, top_k) .map((r, i) => { const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.` const score = ((r.score ?? 0) * 100).toFixed(1) let text = `${medal} Score: ${score}%` if (r.title) text += `\n ${r.title}` if (r.snippet) text += `\n ${r.snippet.substring(0, 150)}...` return text }) .join('\n\n') log('info', `Search returned ${result.results.length} results`) return { content: [{ type: 'text', text: `Found ${result.results.length} result(s) for: "${query}"\n\n${formattedResults}` }], structuredContent: output } } catch (error) { log('error', 'Search failed', { project_id, query, error: error instanceof Error ? error.message : error }) return { content: [{ type: 'text', text: `✗ Search failed: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true } } } ) // Tool 6: Validate Search Quality server.registerTool( 'coderswap_validate_search', { title: 'Validate CoderSwap Search Quality', description: 'Run validation queries to test search quality and coverage (non-DSL quality check)', inputSchema: { project_id: z.string().min(1, 'project_id is required'), test_queries: z.array(z.string()).optional(), run_full_suite: z.boolean().default(false) }, outputSchema: { queries_tested: z.number(), average_top_score: z.number(), zero_result_queries: z.array(z.string()) } }, async ({ project_id, test_queries, run_full_suite = false }) => { try { log('debug', 'Testing search quality', { project_id, run_full_suite }) const report = await client.testSearchQuality({ project_id, test_queries, run_full_suite }) const output = { queries_tested: report.aggregate.queries_tested, average_top_score: report.aggregate.average_top_score, zero_result_queries: report.aggregate.zero_result_queries } const avgScore = (report.aggregate.average_top_score * 100).toFixed(1) const zeroResults = report.aggregate.zero_result_queries.length let summary = `Search Quality Report\n${'='.repeat(40)}\n` summary += `Queries tested: ${report.aggregate.queries_tested}\n` summary += `Average top score: ${avgScore}%\n` summary += `Zero-result queries: ${zeroResults}\n\n` if (report.results.length > 0) { summary += 'Top Results:\n' report.results.slice(0, 3).forEach(r => { const score = (r.topScore * 100).toFixed(1) summary += ` • "${r.query}" → ${score}% (${r.count} results)\n` }) } log('info', `Search quality test completed: ${report.aggregate.queries_tested} queries`) return { content: [{ type: 'text', text: summary }], structuredContent: output } } catch (error) { log('error', 'Search quality test failed', { project_id, error: error instanceof Error ? error.message : error }) return { content: [{ type: 'text', text: `✗ Search quality test failed: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true } } } ) // Tool 7: Log Session Note server.registerTool( 'coderswap_log_session_note', { title: 'Log Session Note', description: 'Record lightweight ingestion summary for session continuity (non-DSL)', inputSchema: { project_id: z.string().min(1, 'project_id is required'), summary_text: z.string().min(1, 'summary_text is required'), job_id: z.string().optional(), ingestion_metrics: z.any().optional(), tags: z.any().optional() }, outputSchema: { note_id: z.string(), project_id: z.string(), timestamp: z.string() } }, async ({ project_id, summary_text, job_id, ingestion_metrics, tags }) => { if (containsGuardrailBypass({ project_id, summary_text, job_id, ingestion_metrics, tags })) { return guardrailViolationResponse() } try { log('debug', 'Logging session note', { project_id, job_id }) // Generate a simple note ID const timestamp = new Date().toISOString() const note_id = `note_${Date.now()}` // Log the note (for now, just to console/debug) log('info', `Session note logged for project ${project_id}`, { note_id, summary_text, job_id, ingestion_metrics, tags }) const output = { note_id, project_id, timestamp } return { content: [{ type: 'text', text: `✓ Logged session note: ${summary_text.substring(0, 100)}${summary_text.length > 100 ? '...' : ''}` }], structuredContent: output } } catch (error) { log('error', 'Failed to log session note', { project_id, error: error instanceof Error ? error.message : error }) return { content: [{ type: 'text', text: `✗ Failed to log session note: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true } } } ) // Start the server with stdio transport const transport = new StdioServerTransport() await server.connect(transport) log('info', 'CoderSwap MCP Server ready and listening on stdio')

Implementation Reference

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/njlnaet/mcp-server'

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