From 1ccdcac84a4ccb26b531e3dd124157ad2aa5f745 Mon Sep 17 00:00:00 2001 From: Blake Martin Date: Wed, 6 Aug 2025 11:39:57 -0400 Subject: [PATCH 1/2] Add prompts registration and management to MCP server - Introduced MCPPrompt interface and prompt management functions. - Implemented prompt registration in the tool registry. - Enhanced server capabilities with prompt handling. - Removed obsolete test-http.js file and added settings.local.json. - Updated CLAUDE.md with detailed project and development information. --- .claude/settings.local.json | 10 ++ CLAUDE.md | 128 ++++++++++++++++ src/index.ts | 1 + src/server/mcpServer.ts | 1 + src/server/toolRegistry.ts | 64 +++++++- src/tools/prompts/prompts.ts | 279 +++++++++++++++++++++++++++++++++++ test-http.js | 74 ---------- 7 files changed, 482 insertions(+), 75 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 CLAUDE.md create mode 100644 src/tools/prompts/prompts.ts delete mode 100644 test-http.js diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..67ea9a6 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run build:*)", + "WebFetch(domain:github.com)", + "Bash(npm run lint)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b86181f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,128 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a **Model Context Protocol (MCP) server** for CData Connect Cloud API integration. It enables AI agents to interact with cloud-connected data sources through SQL queries, metadata introspection, and stored procedure execution. + +## Development Commands + +### Build and Development +- `npm run build` - Compile TypeScript to dist/ +- `npm run dev` - Start development server with ts-node (default: HTTP transport) +- `npm run dev:http` - Start development server with HTTP transport +- `npm run dev:stdio` - Start development server with STDIO transport +- `npm run watch` - Watch TypeScript files and recompile on changes +- `npm run clean` - Remove dist/ directory + +### Production +- `npm start` - Start compiled server (default: HTTP transport) +- `npm run start:http` - Start with HTTP transport +- `npm run start:stdio` - Start with STDIO transport +- `npm run build-start` - Build then start production server + +### Code Quality +- `npm run lint` - Run ESLint with auto-fix +- `npm run format` - Format code with Prettier +- `npm run lint:format` - Run both lint and format + +### Testing and Debugging +- `npm run inspector` - Launch MCP Inspector web UI +- `npm run inspector:stdio` - Launch inspector with STDIO transport +- `npm run inspector:http` - Launch inspector with HTTP transport (requires separate server start) +- `npm run inspector:cli` - CLI mode inspector with STDIO transport +- `npm run test:inspector` - Quick automated inspector test +- `npm run validate:inspector` - Validate inspector configuration + +## Architecture + +### Core Components + +**Entry Point**: `src/index.ts` +- Global request ID tracking +- Environment configuration loading +- Transport initialization + +**MCP Server**: `src/server/mcpServer.ts` +- Basic MCP server instance using @modelcontextprotocol/sdk +- Server name: "CData Connect Cloud" + +**Transport Layer**: `src/transports/` +- `index.ts` - Transport selection logic (HTTP vs STDIO) +- `httpTransport.ts` - HTTP server with Express.js endpoints +- `stdioTransport.ts` - Standard I/O transport for CLI usage + +**Tool Registry**: `src/server/toolRegistry.ts` +- Registers all MCP tools with the server +- Handles tool parameter validation using Zod schemas +- Error handling and response formatting + +### Tool Categories + +**Data Operations** (`src/tools/query/`): +- `queryData` - Execute SQL queries with optional parameters +- `execData` - Execute stored procedures + +**Metadata Operations** (`src/tools/metadata/`): +- `getCatalogs` - List available connections/catalogs +- `getSchemas` - List schemas within catalogs +- `getTables` - List tables within schemas +- `getColumns` - Get column metadata for tables +- `getPrimaryKeys` - Retrieve primary key information +- `getIndexes` - Get index information +- `getImportedKeys` / `getExportedKeys` - Foreign key relationships +- `getProcedures` - List stored procedures +- `getProcedureParameters` - Get procedure parameter information + +### HTTP Transport Endpoints + +When using HTTP transport (default), the server provides: +- `/mcp` - Primary MCP endpoint with session management +- `/direct` - Direct JSON-RPC endpoint without sessions +- `/.well-known/mc/manifest.json` - MCP discovery manifest + +### Configuration + +**Environment Variables**: +- `CDATA_USERNAME` - CData Connect Cloud username (required) +- `CDATA_PAT` - Personal Access Token (required) +- `CDATA_API_URL` - API URL (default: https://cloud.cdata.com/api) +- `TRANSPORT_TYPE` - "http" or "stdio" (default: "http") +- `PORT` - HTTP server port (default: 3000) +- `HOST` - HTTP server host (default: localhost) +- `LOG_ENABLED` - Enable logging (default: false) +- `LOG_LEVEL` - Log level (default: info) + +**Inspector Configuration**: `mcp-inspector.json` +- Pre-configured for both STDIO and HTTP transports +- Used by MCP Inspector for testing and debugging + +## Development Workflow + +1. **Environment Setup**: Create `.env` file with required CData credentials +2. **Development**: Use `npm run dev` for live development with HTTP transport +3. **Testing**: Use `npm run inspector` to test tools with MCP Inspector +4. **Code Quality**: Run `npm run lint:format` before commits +5. **Build**: Use `npm run build` to compile for production + +## Key Dependencies + +- `@modelcontextprotocol/sdk` - Core MCP functionality +- `axios` - HTTP client for CData API requests +- `express` - HTTP server framework +- `winston` - Logging framework +- `zod` - Schema validation for tool parameters +- `dotenv` - Environment variable management + +## Transport Modes + +**HTTP Transport** (Default): +- Better for integration with web-based AI clients +- Provides REST-like endpoints and MCP protocol endpoints +- Session management and connection pooling + +**STDIO Transport**: +- Direct stdin/stdout communication +- Better for CLI tools and direct process communication +- Used by Claude Desktop and similar clients \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index c08c12a..92288e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { setupTransport } from './transports'; // Declare a global variable to track the current request ID declare global { + // eslint-disable-next-line no-var var currentRequestId: string | number | null; } global.currentRequestId = null; diff --git a/src/server/mcpServer.ts b/src/server/mcpServer.ts index ab50737..08eaaaa 100644 --- a/src/server/mcpServer.ts +++ b/src/server/mcpServer.ts @@ -8,6 +8,7 @@ const server = new McpServer({ version: '1.0.5', capabilities: { tools: {}, + prompts: {}, }, }); diff --git a/src/server/toolRegistry.ts b/src/server/toolRegistry.ts index 64af6b5..9fc5c4d 100644 --- a/src/server/toolRegistry.ts +++ b/src/server/toolRegistry.ts @@ -13,12 +13,15 @@ import { getSchemas, getTables, } from '../tools/metadata'; +import { getPrompts, generatePromptMessages } from '../tools/prompts/prompts'; /** - * Register all tools with the MCP server + * Register all tools and prompts with the MCP server * @param server The MCP server instance */ export function registerTools(server: McpServer) { + // Register prompts + registerPrompts(server); // Query Data tool server.tool( 'queryData', @@ -405,3 +408,62 @@ export function registerTools(server: McpServer) { }, ); } + +/** + * Register prompts with the MCP server + * @param server The MCP server instance + */ +function registerPrompts(server: McpServer) { + const prompts = getPrompts(); + + // Register each prompt individually + prompts.forEach(prompt => { + // Convert prompt arguments to Zod schema + const argsSchema: Record = {}; + + if (prompt.arguments) { + prompt.arguments.forEach(arg => { + // Convert to Zod string schema, make optional if not required + argsSchema[arg.name] = arg.required + ? z.string().describe(arg.description || '') + : z + .string() + .optional() + .describe(arg.description || ''); + }); + } + + // Register the prompt using the correct API + if (Object.keys(argsSchema).length > 0) { + // Prompt with arguments + server.prompt(prompt.name, prompt.description || '', argsSchema, args => { + const messages = generatePromptMessages(prompt.name, args || {}); + return { + description: prompt.description || '', + messages: messages.map(msg => ({ + role: msg.role as 'user' | 'assistant', + content: { + type: 'text' as const, + text: msg.content.text, + }, + })), + }; + }); + } else { + // Prompt without arguments + server.prompt(prompt.name, prompt.description || '', () => { + const messages = generatePromptMessages(prompt.name, {}); + return { + description: prompt.description || '', + messages: messages.map(msg => ({ + role: msg.role as 'user' | 'assistant', + content: { + type: 'text' as const, + text: msg.content.text, + }, + })), + }; + }); + } + }); +} diff --git a/src/tools/prompts/prompts.ts b/src/tools/prompts/prompts.ts new file mode 100644 index 0000000..f1b0344 --- /dev/null +++ b/src/tools/prompts/prompts.ts @@ -0,0 +1,279 @@ +import { debug, error } from '../../utils/logger'; + +/** + * Prompt interface following MCP specification + */ +export interface MCPPrompt { + name: string; // Unique identifier for the prompt + description?: string; // Human-readable description + arguments?: { + name: string; // Argument identifier + description?: string; // Argument description + required?: boolean; // Whether argument is required + }[]; +} + +/** + * Database query best practices prompts + */ +const DATABASE_PROMPTS: Record = { + 'limit-results': { + name: 'limit-results', + description: 'Get guidance on limiting result sets when querying databases', + arguments: [], + }, + 'explore-schema': { + name: 'explore-schema', + description: 'Get workflow for exploring an unfamiliar database schema', + arguments: [], + }, + pagination: { + name: 'pagination', + description: 'Get patterns for implementing pagination with SQL queries', + arguments: [ + { + name: 'dialect', + description: 'SQL dialect (mysql, postgresql, sqlserver)', + required: false, + }, + ], + }, + 'parameterized-queries': { + name: 'parameterized-queries', + description: 'Get guidance on using parameters in SQL queries to prevent SQL injection', + arguments: [], + }, + 'handle-results': { + name: 'handle-results', + description: 'Get strategies for handling empty or large result sets', + arguments: [], + }, + 'error-handling': { + name: 'error-handling', + description: 'Get troubleshooting steps for database query errors', + arguments: [], + }, +}; + +/** + * Get all available prompts + * @returns List of available prompts + */ +export function getPrompts(): MCPPrompt[] { + debug('Retrieving all available prompts'); + return Object.values(DATABASE_PROMPTS); +} + +/** + * Get a specific prompt by name + * @param name - Prompt name to retrieve + * @returns The prompt if found, or undefined if not found + */ +export function getPromptByName(name: string): MCPPrompt | undefined { + debug(`Retrieving prompt with name: ${name}`); + return DATABASE_PROMPTS[name]; +} + +/** + * Generate prompt messages based on the prompt name and arguments + * @param name - Prompt name + * @param args - Prompt arguments + * @returns Array of messages for the prompt + */ +export function generatePromptMessages( + name: string, + args: Record = {}, +): Array<{ role: string; content: { type: string; text: string } }> { + debug(`Generating messages for prompt: ${name} with args: ${JSON.stringify(args)}`); + + switch (name) { + case 'limit-results': + return [ + { + role: 'user', + content: { + type: 'text', + text: 'Provide guidance on limiting result sets when querying databases to prevent excessive data retrieval.', + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: `# Result Set Limiting +When using the \`queryData\` tool, always include LIMIT/TOP clauses in your queries to prevent excessive result sets. For example: +\`\`\`sql +SELECT * FROM table_name LIMIT 10; +-- or for SQL Server +SELECT TOP 10 * FROM table_name; +\`\`\` +Limiting results to 10-20 rows is recommended unless specifically instructed otherwise.`, + }, + }, + ]; + + case 'explore-schema': + return [ + { + role: 'user', + content: { + type: 'text', + text: "What's the recommended workflow for exploring an unfamiliar database schema?", + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: `# Schema Exploration Workflow +Before querying an unfamiliar database: +1. Use \`getCatalogs\` to identify available data sources +2. Use \`getSchemas\` with the relevant catalog to find available schemas +3. Use \`getTables\` to discover tables within the schema of interest +4. Use \`getColumns\` to understand the structure of specific tables`, + }, + }, + ]; + + case 'pagination': + const dialect = args.dialect || 'standard'; + let paginationExample; + + if (dialect === 'sqlserver') { + paginationExample = `-- SQL Server pagination (2012+) +-- Initial query +SELECT * FROM table_name ORDER BY id OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY; +-- Subsequent queries +SELECT * FROM table_name ORDER BY id OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;`; + } else { + paginationExample = `-- Standard SQL pagination +-- Initial query +SELECT * FROM table_name ORDER BY id LIMIT 10 OFFSET 0; +-- Subsequent queries +SELECT * FROM table_name ORDER BY id LIMIT 10 OFFSET 10;`; + } + + return [ + { + role: 'user', + content: { + type: 'text', + text: `Provide patterns for implementing pagination with SQL queries for ${dialect} dialect.`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: `# Pagination for Large Result Sets +For queries that may return large result sets, implement pagination: +\`\`\`sql +${paginationExample} +\`\`\` +This approach allows you to retrieve data in manageable chunks, improving performance and user experience.`, + }, + }, + ]; + + case 'parameterized-queries': + return [ + { + role: 'user', + content: { + type: 'text', + text: 'How should I use parameters in SQL queries to prevent SQL injection?', + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: `# Parameterized Query Usage +Always use parameters for dynamic values to prevent SQL injection: +\`\`\` +-- Query with parameters +SELECT * FROM customers WHERE region = @region AND status = @status LIMIT 10; +-- Parameters object +{ + "@region": "West", + "@status": "Active" +} +\`\`\` +Never concatenate user input directly into SQL strings. Always use the parameters object to pass variable data.`, + }, + }, + ]; + + case 'handle-results': + return [ + { + role: 'user', + content: { + type: 'text', + text: 'What strategies should I use for handling empty or large result sets?', + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: `# Handling Empty or Large Results +Consider these strategies when dealing with potentially empty or large result sets: +- Check if results are empty before proceeding with analysis +- For large results, consider aggregations or filtering before fetching +- Use COUNT(*) queries to understand result size before fetching all rows +Example to check result size first: +\`\`\`sql +SELECT COUNT(*) as total_count FROM table_name WHERE condition; +\`\`\` +If the count is manageable, proceed with the actual query: +\`\`\`sql +SELECT * FROM table_name WHERE condition LIMIT 100; +\`\`\``, + }, + }, + ]; + + case 'error-handling': + return [ + { + role: 'user', + content: { + type: 'text', + text: 'What troubleshooting steps should I take when a database query fails?', + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: `# Error Handling +If a query fails, try these troubleshooting steps: +1. Checking table and column names + - Verify spelling and case sensitivity + - Ensure the table exists in the current schema +2. Verifying parameter formats + - Check data types match expected formats + - Ensure date formats are correct +3. Simplifying the query complexity + - Break down complex queries into simpler parts + - Test individual components separately +4. Ensuring proper permissions for the requested operation + - Verify connection has SELECT, INSERT, UPDATE permissions as needed`, + }, + }, + ]; + + default: + error(`Unknown prompt name: ${name}`); + return [ + { + role: 'assistant', + content: { + type: 'text', + text: `Error: Prompt '${name}' is not available.`, + }, + }, + ]; + } +} diff --git a/test-http.js b/test-http.js deleted file mode 100644 index 7d9a87f..0000000 --- a/test-http.js +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env node - -/** - * Simple test script to verify the MCP server HTTP transport is working - */ - -const serverUrl = 'http://localhost:3000'; - -async function testManifest() { - console.log('šŸ” Testing manifest endpoint...'); - try { - const response = await fetch(`${serverUrl}/.well-known/mc/manifest.json`); - const manifest = await response.json(); - console.log('āœ… Manifest endpoint working:', manifest); - return true; - } catch (error) { - console.error('āŒ Manifest endpoint failed:', error.message); - return false; - } -} - -async function testDirectEndpoint() { - console.log('šŸ” Testing direct endpoint...'); - try { - const response = await fetch(`${serverUrl}/direct`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - jsonrpc: '2.0', - method: 'initialize', - params: { - protocolVersion: '2024-11-05', - capabilities: {}, - clientInfo: { - name: 'test-client', - version: '1.0.0' - } - }, - id: 1 - }) - }); - - const result = await response.json(); - console.log('āœ… Direct endpoint response:', result); - return true; - } catch (error) { - console.error('āŒ Direct endpoint failed:', error.message); - return false; - } -} - -async function main() { - console.log('šŸš€ Testing CData Connect Cloud MCP Server HTTP Transport\n'); - - const manifestTest = await testManifest(); - if (!manifestTest) { - console.log('āŒ Server may not be running. Please start with: npm run dev'); - process.exit(1); - } - - console.log(); - await testDirectEndpoint(); - - console.log('\nāœ… HTTP Transport tests completed!'); - console.log('šŸ“ The server is running on streamable-http transport successfully.'); - console.log(`🌐 Access the server at: ${serverUrl}`); - console.log(`šŸ“‹ Manifest: ${serverUrl}/.well-known/mc/manifest.json`); - console.log(`šŸ”§ MCP Endpoint: ${serverUrl}/mcp`); - console.log(`⚔ Direct Endpoint: ${serverUrl}/direct`); -} - -main().catch(console.error); From 783e9087e8feb40f948dc90cb6df29bafafdf24d Mon Sep 17 00:00:00 2001 From: Blake Martin Date: Thu, 7 Aug 2025 09:55:42 -0400 Subject: [PATCH 2/2] updating package-lock.json --- .claude/settings.local.json | 4 +++- package-lock.json | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 67ea9a6..ca9d93e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,9 @@ "allow": [ "Bash(npm run build:*)", "WebFetch(domain:github.com)", - "Bash(npm run lint)" + "Bash(npm run lint)", + "Bash(TRANSPORT_TYPE=stdio node dist/index.js --help)", + "Bash(tasklist:*)" ], "deny": [] } diff --git a/package-lock.json b/package-lock.json index b75f2e6..17a51dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cdatasoftware/connectcloud-mcp-server", - "version": "1.0.5", + "version": "1.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cdatasoftware/connectcloud-mcp-server", - "version": "1.0.5", + "version": "1.0.6", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.10.2",