From 3fbb9a18372f2b6a675dd6c039ba52be76f3eeb4 Mon Sep 17 00:00:00 2001 From: TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> Date: Fri, 16 Jan 2026 08:30:14 +0900 Subject: updates --- .../.claude/commands/add-prompt.md | 242 +++++++++++++ .../.claude/commands/add-resource.md | 243 +++++++++++++ .../simple-mcp-server/.claude/commands/add-tool.md | 207 +++++++++++ .../simple-mcp-server/.claude/commands/build.md | 377 +++++++++++++++++++++ .../simple-mcp-server/.claude/commands/debug.md | 310 +++++++++++++++++ .../simple-mcp-server/.claude/commands/deploy.md | 376 ++++++++++++++++++++ .../simple-mcp-server/.claude/commands/init.md | 178 ++++++++++ .../simple-mcp-server/.claude/commands/test.md | 261 ++++++++++++++ 8 files changed, 2194 insertions(+) create mode 100644 mcp-servers/simple-mcp-server/.claude/commands/add-prompt.md create mode 100644 mcp-servers/simple-mcp-server/.claude/commands/add-resource.md create mode 100644 mcp-servers/simple-mcp-server/.claude/commands/add-tool.md create mode 100644 mcp-servers/simple-mcp-server/.claude/commands/build.md create mode 100644 mcp-servers/simple-mcp-server/.claude/commands/debug.md create mode 100644 mcp-servers/simple-mcp-server/.claude/commands/deploy.md create mode 100644 mcp-servers/simple-mcp-server/.claude/commands/init.md create mode 100644 mcp-servers/simple-mcp-server/.claude/commands/test.md (limited to 'mcp-servers/simple-mcp-server/.claude/commands') diff --git a/mcp-servers/simple-mcp-server/.claude/commands/add-prompt.md b/mcp-servers/simple-mcp-server/.claude/commands/add-prompt.md new file mode 100644 index 0000000..5f9f007 --- /dev/null +++ b/mcp-servers/simple-mcp-server/.claude/commands/add-prompt.md @@ -0,0 +1,242 @@ +# Add Prompt Template to MCP Server + +Adds a new prompt template to your MCP server for reusable conversation patterns. + +## Usage + +``` +/add-prompt [arguments] +``` + +## Examples + +``` +/add-prompt code_review "Review code for improvements" language:string file:string? +/add-prompt analyze_data "Analyze data patterns" dataset:string metrics:array +/add-prompt generate_docs "Generate documentation" codebase:string style:enum[minimal,detailed] +``` + +## Implementation + +```typescript +import * as fs from 'fs/promises'; +import * as path from 'path'; + +async function addPrompt( + name: string, + description: string, + argumentDefs?: string[] +) { + // Parse arguments + const args = parsePromptArguments(argumentDefs || []); + + // Generate prompt file + const promptContent = generatePromptFile(name, description, args); + + // Write prompt file + const promptPath = path.join('src/prompts', `${name}.ts`); + await fs.writeFile(promptPath, promptContent); + + // Update prompt index + await updatePromptIndex(name); + + // Generate test file + const testContent = generatePromptTest(name); + const testPath = path.join('tests/unit/prompts', `${name}.test.ts`); + await fs.writeFile(testPath, testContent); + + console.log(`โœ… Prompt "${name}" added successfully!`); + console.log(` - Implementation: ${promptPath}`); + console.log(` - Test file: ${testPath}`); + console.log(`\nNext steps:`); + console.log(` 1. Define the prompt template in ${promptPath}`); + console.log(` 2. Test with MCP Inspector`); +} + +interface PromptArgument { + name: string; + description: string; + required: boolean; + type: string; +} + +function parsePromptArguments(argumentDefs: string[]): PromptArgument[] { + return argumentDefs.map(def => { + const [nameWithType, description] = def.split('='); + const [nameType] = nameWithType.split(':'); + const isOptional = nameType.endsWith('?'); + const name = isOptional ? nameType.slice(0, -1) : nameType; + const type = nameWithType.split(':')[1] || 'string'; + + return { + name, + description: description || `${name} parameter`, + required: !isOptional, + type: type.replace('?', ''), + }; + }); +} + +function generatePromptFile( + name: string, + description: string, + args: PromptArgument[] +): string { + return ` +import type { Prompt, PromptMessage } from '../types/prompts.js'; + +export const ${name}Prompt: Prompt = { + name: '${name}', + description: '${description}', + arguments: [ +${args.map(arg => ` { + name: '${arg.name}', + description: '${arg.description}', + required: ${arg.required}, + },`).join('\n')} + ], +}; + +export function get${capitalize(name)}Prompt( + args: Record +): PromptMessage[] { + // Validate required arguments +${args.filter(a => a.required).map(arg => ` if (!args.${arg.name}) { + throw new Error('Missing required argument: ${arg.name}'); + }`).join('\n')} + + // Build prompt messages + const messages: PromptMessage[] = []; + + // System message (optional) + messages.push({ + role: 'system', + content: { + type: 'text', + text: buildSystemPrompt(args), + }, + }); + + // User message + messages.push({ + role: 'user', + content: { + type: 'text', + text: buildUserPrompt(args), + }, + }); + + return messages; +} + +function buildSystemPrompt(args: Record): string { + // TODO: Define the system prompt template + return \`You are an expert assistant helping with ${description.toLowerCase()}.\`; +} + +function buildUserPrompt(args: Record): string { + // TODO: Define the user prompt template + let prompt = '${description}\\n\\n'; + +${args.map(arg => ` if (args.${arg.name}) { + prompt += \`${capitalize(arg.name)}: \${args.${arg.name}}\\n\`; + }`).join('\n')} + + return prompt; +} + +function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} +`; +} + +function generatePromptTest(name: string): string { + return ` +import { describe, it, expect } from 'vitest'; +import { ${name}Prompt, get${capitalize(name)}Prompt } from '../../src/prompts/${name}.js'; + +describe('${name} prompt', () => { + it('should have correct metadata', () => { + expect(${name}Prompt.name).toBe('${name}'); + expect(${name}Prompt.description).toBeDefined(); + expect(${name}Prompt.arguments).toBeDefined(); + }); + + it('should generate prompt messages', () => { + const args = { + // TODO: Add test arguments + }; + + const messages = get${capitalize(name)}Prompt(args); + + expect(messages).toBeInstanceOf(Array); + expect(messages.length).toBeGreaterThan(0); + expect(messages[0]).toHaveProperty('role'); + expect(messages[0]).toHaveProperty('content'); + }); + + it('should validate required arguments', () => { + const invalidArgs = {}; + + expect(() => get${capitalize(name)}Prompt(invalidArgs)) + .toThrow('Missing required argument'); + }); + + it('should handle optional arguments', () => { + const minimalArgs = { + // Only required args + }; + + const messages = get${capitalize(name)}Prompt(minimalArgs); + expect(messages).toBeDefined(); + }); +}); +`; +} + +function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +async function updatePromptIndex(name: string) { + const indexPath = 'src/prompts/index.ts'; + + try { + let content = await fs.readFile(indexPath, 'utf-8'); + + // Add import + const importLine = `import { ${name}Prompt, get${capitalize(name)}Prompt } from './${name}.js';`; + if (!content.includes(importLine)) { + const lastImport = content.lastIndexOf('import'); + const endOfLastImport = content.indexOf('\n', lastImport); + content = content.slice(0, endOfLastImport + 1) + importLine + '\n' + content.slice(endOfLastImport + 1); + } + + // Add to exports + const exportPattern = /export const prompts = \[([^\]]*)]\;/; + const match = content.match(exportPattern); + if (match) { + const currentExports = match[1].trim(); + const newExports = currentExports ? `${currentExports},\n ${name}Prompt` : `\n ${name}Prompt\n`; + content = content.replace(exportPattern, `export const prompts = [${newExports}];`); + } + + await fs.writeFile(indexPath, content); + } catch (error) { + // Create index file if it doesn't exist + const newIndex = ` +import { ${name}Prompt, get${capitalize(name)}Prompt } from './${name}.js'; + +export const prompts = [ + ${name}Prompt, +]; + +export const promptHandlers = { + '${name}': get${capitalize(name)}Prompt, +}; +`; + await fs.writeFile(indexPath, newIndex); + } +} +``` \ No newline at end of file diff --git a/mcp-servers/simple-mcp-server/.claude/commands/add-resource.md b/mcp-servers/simple-mcp-server/.claude/commands/add-resource.md new file mode 100644 index 0000000..aff9e54 --- /dev/null +++ b/mcp-servers/simple-mcp-server/.claude/commands/add-resource.md @@ -0,0 +1,243 @@ +# Add Resource to MCP Server + +Adds a new resource endpoint to your MCP server with proper URI handling. + +## Usage + +``` +/add-resource [uri-pattern] [mime-type] +``` + +## Examples + +``` +/add-resource config "Server configuration" config://settings application/json +/add-resource users "User database" data://users/{id} application/json +/add-resource files "File system access" file:///{path} text/plain +``` + +## Implementation + +```typescript +import * as fs from 'fs/promises'; +import * as path from 'path'; + +async function addResource( + name: string, + description: string, + uriPattern?: string, + mimeType: string = 'application/json' +) { + // Generate URI pattern if not provided + const uri = uriPattern || `${name}://default`; + + // Generate resource file + const resourceContent = generateResourceFile(name, description, uri, mimeType); + + // Write resource file + const resourcePath = path.join('src/resources', `${name}.ts`); + await fs.writeFile(resourcePath, resourceContent); + + // Update resource index + await updateResourceIndex(name); + + // Generate test file + const testContent = generateResourceTest(name, uri); + const testPath = path.join('tests/unit/resources', `${name}.test.ts`); + await fs.writeFile(testPath, testContent); + + console.log(`โœ… Resource "${name}" added successfully!`); + console.log(` - Implementation: ${resourcePath}`); + console.log(` - Test file: ${testPath}`); + console.log(` - URI pattern: ${uri}`); + console.log(` - MIME type: ${mimeType}`); + console.log(`\nNext steps:`); + console.log(` 1. Implement the resource provider in ${resourcePath}`); + console.log(` 2. Test with MCP Inspector`); +} + +function generateResourceFile( + name: string, + description: string, + uri: string, + mimeType: string +): string { + const hasDynamicParams = uri.includes('{'); + + return ` +import type { Resource, ResourceContent } from '../types/resources.js'; + +export const ${name}Resource: Resource = { + uri: '${uri}', + name: '${name}', + description: '${description}', + mimeType: '${mimeType}', +}; + +export async function read${capitalize(name)}Resource( + uri: string +): Promise { + ${hasDynamicParams ? generateDynamicResourceHandler(uri) : generateStaticResourceHandler()} + + return [ + { + uri, + mimeType: '${mimeType}', + text: ${mimeType === 'application/json' ? 'JSON.stringify(data, null, 2)' : 'data'}, + }, + ]; +} + +${generateResourceDataFunction(name, mimeType)} +`; +} + +function generateDynamicResourceHandler(uriPattern: string): string { + return ` + // Parse dynamic parameters from URI + const params = parseUriParams('${uriPattern}', uri); + + // Fetch data based on parameters + const data = await fetch${capitalize(name)}Data(params); + + if (!data) { + throw new Error(\`Resource not found: \${uri}\`); + } +`; +} + +function generateStaticResourceHandler(): string { + return ` + // Fetch static resource data + const data = await fetch${capitalize(name)}Data(); +`; +} + +function generateResourceDataFunction(name: string, mimeType: string): string { + if (mimeType === 'application/json') { + return ` +async function fetch${capitalize(name)}Data(params?: Record) { + // TODO: Implement data fetching logic + // This is a placeholder implementation + + if (params?.id) { + // Return specific item + return { + id: params.id, + name: 'Example Item', + timestamp: new Date().toISOString(), + }; + } + + // Return collection + return { + items: [ + { id: '1', name: 'Item 1' }, + { id: '2', name: 'Item 2' }, + ], + total: 2, + }; +} + +function parseUriParams(pattern: string, uri: string): Record { + // Convert pattern to regex + const regexPattern = pattern + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + .replace(/\{(\w+)\}/g, '(?<$1>[^/]+)'); + + const regex = new RegExp(\`^\${regexPattern}$\`); + const match = uri.match(regex); + + return match?.groups || {}; +} +`; + } else { + return ` +async function fetch${capitalize(name)}Data(params?: Record) { + // TODO: Implement data fetching logic + // This is a placeholder implementation + + return 'Resource content as ${mimeType}'; +} +`; + } +} + +function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +function generateResourceTest(name: string, uri: string): string { + return ` +import { describe, it, expect } from 'vitest'; +import { ${name}Resource, read${capitalize(name)}Resource } from '../../src/resources/${name}.js'; + +describe('${name} resource', () => { + it('should have correct metadata', () => { + expect(${name}Resource.name).toBe('${name}'); + expect(${name}Resource.uri).toBe('${uri}'); + expect(${name}Resource.description).toBeDefined(); + expect(${name}Resource.mimeType).toBeDefined(); + }); + + it('should read resource content', async () => { + const content = await read${capitalize(name)}Resource('${uri.replace('{id}', 'test-id')}'); + + expect(content).toBeInstanceOf(Array); + expect(content[0]).toHaveProperty('uri'); + expect(content[0]).toHaveProperty('mimeType'); + expect(content[0]).toHaveProperty('text'); + }); + + it('should handle missing resources', async () => { + // TODO: Add tests for missing resources + }); + + it('should validate URI format', () => { + // TODO: Add URI validation tests + }); +}); +`; +} + +async function updateResourceIndex(name: string) { + const indexPath = 'src/resources/index.ts'; + + try { + let content = await fs.readFile(indexPath, 'utf-8'); + + // Add import + const importLine = `import { ${name}Resource, read${capitalize(name)}Resource } from './${name}.js';`; + if (!content.includes(importLine)) { + const lastImport = content.lastIndexOf('import'); + const endOfLastImport = content.indexOf('\n', lastImport); + content = content.slice(0, endOfLastImport + 1) + importLine + '\n' + content.slice(endOfLastImport + 1); + } + + // Add to exports + const exportPattern = /export const resources = \[([^\]]*)]\;/; + const match = content.match(exportPattern); + if (match) { + const currentExports = match[1].trim(); + const newExports = currentExports ? `${currentExports},\n ${name}Resource` : `\n ${name}Resource\n`; + content = content.replace(exportPattern, `export const resources = [${newExports}];`); + } + + await fs.writeFile(indexPath, content); + } catch (error) { + // Create index file if it doesn't exist + const newIndex = ` +import { ${name}Resource, read${capitalize(name)}Resource } from './${name}.js'; + +export const resources = [ + ${name}Resource, +]; + +export const resourceReaders = { + '${name}': read${capitalize(name)}Resource, +}; +`; + await fs.writeFile(indexPath, newIndex); + } +} +``` \ No newline at end of file diff --git a/mcp-servers/simple-mcp-server/.claude/commands/add-tool.md b/mcp-servers/simple-mcp-server/.claude/commands/add-tool.md new file mode 100644 index 0000000..da81212 --- /dev/null +++ b/mcp-servers/simple-mcp-server/.claude/commands/add-tool.md @@ -0,0 +1,207 @@ +# Add Tool to MCP Server + +Adds a new tool to your MCP server with proper schema validation and error handling. + +## Usage + +``` +/add-tool [parameters] +``` + +## Examples + +``` +/add-tool calculate "Performs mathematical calculations" +/add-tool search "Search for information" query:string limit:number? +/add-tool process_data "Process data with options" input:string format:enum[json,csv,xml] +``` + +## Implementation + +```typescript +import { z } from 'zod'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +async function addTool(name: string, description: string, parameters?: string[]) { + // Parse parameters into schema + const schema = parseParameterSchema(parameters || []); + + // Generate tool file + const toolContent = generateToolFile(name, description, schema); + + // Write tool file + const toolPath = path.join('src/tools', `${name}.ts`); + await fs.writeFile(toolPath, toolContent); + + // Update tool index + await updateToolIndex(name); + + // Generate test file + const testContent = generateToolTest(name); + const testPath = path.join('tests/unit/tools', `${name}.test.ts`); + await fs.writeFile(testPath, testContent); + + console.log(`โœ… Tool "${name}" added successfully!`); + console.log(` - Implementation: ${toolPath}`); + console.log(` - Test file: ${testPath}`); + console.log(`\nNext steps:`); + console.log(` 1. Implement the tool logic in ${toolPath}`); + console.log(` 2. Run tests with "npm test"`); + console.log(` 3. Test with MCP Inspector`); +} + +function parseParameterSchema(parameters: string[]): any { + const properties: Record = {}; + const required: string[] = []; + + for (const param of parameters) { + const [nameType, ...rest] = param.split(':'); + const isOptional = nameType.endsWith('?'); + const name = isOptional ? nameType.slice(0, -1) : nameType; + const type = rest.join(':') || 'string'; + + if (!isOptional) { + required.push(name); + } + + properties[name] = parseType(type); + } + + return { + type: 'object', + properties, + required: required.length > 0 ? required : undefined, + }; +} + +function parseType(type: string): any { + if (type.startsWith('enum[')) { + const values = type.slice(5, -1).split(','); + return { + type: 'string', + enum: values, + }; + } + + switch (type) { + case 'number': + return { type: 'number' }; + case 'boolean': + return { type: 'boolean' }; + case 'array': + return { type: 'array', items: { type: 'string' } }; + default: + return { type: 'string' }; + } +} + +function generateToolFile(name: string, description: string, schema: any): string { + return ` +import { z } from 'zod'; +import type { ToolHandler } from '../types/tools.js'; + +// Define Zod schema for validation +const ${capitalize(name)}Schema = z.object({ +${generateZodSchema(schema.properties, ' ')} +}); + +export type ${capitalize(name)}Args = z.infer; + +export const ${name}Tool = { + name: '${name}', + description: '${description}', + inputSchema: ${JSON.stringify(schema, null, 2)}, + handler: async (args: unknown): Promise => { + // Validate input + const validated = ${capitalize(name)}Schema.parse(args); + + // TODO: Implement your tool logic here + const result = await process${capitalize(name)}(validated); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(result), + }, + ], + }; + }, +}; + +async function process${capitalize(name)}(args: ${capitalize(name)}Args) { + // TODO: Implement the actual processing logic + return { + success: true, + message: 'Tool executed successfully', + input: args, + }; +} +`; +} + +function generateZodSchema(properties: Record, indent: string): string { + const lines: string[] = []; + + for (const [key, value] of Object.entries(properties)) { + let zodType = 'z.string()'; + + if (value.type === 'number') { + zodType = 'z.number()'; + } else if (value.type === 'boolean') { + zodType = 'z.boolean()'; + } else if (value.enum) { + zodType = `z.enum([${value.enum.map((v: string) => `'${v}'`).join(', ')}])`; + } else if (value.type === 'array') { + zodType = 'z.array(z.string())'; + } + + lines.push(`${indent}${key}: ${zodType},`); + } + + return lines.join('\n'); +} + +function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +function generateToolTest(name: string): string { + return ` +import { describe, it, expect, vi } from 'vitest'; +import { ${name}Tool } from '../../src/tools/${name}.js'; + +describe('${name} tool', () => { + it('should have correct metadata', () => { + expect(${name}Tool.name).toBe('${name}'); + expect(${name}Tool.description).toBeDefined(); + expect(${name}Tool.inputSchema).toBeDefined(); + }); + + it('should validate input parameters', async () => { + const invalidInput = {}; + + await expect(${name}Tool.handler(invalidInput)) + .rejects + .toThrow(); + }); + + it('should handle valid input', async () => { + const validInput = { + // TODO: Add valid test input + }; + + const result = await ${name}Tool.handler(validInput); + + expect(result).toHaveProperty('content'); + expect(result.content[0]).toHaveProperty('type', 'text'); + }); + + it('should handle errors gracefully', async () => { + // TODO: Add error handling tests + }); +}); +`; +} +``` \ No newline at end of file diff --git a/mcp-servers/simple-mcp-server/.claude/commands/build.md b/mcp-servers/simple-mcp-server/.claude/commands/build.md new file mode 100644 index 0000000..ed99096 --- /dev/null +++ b/mcp-servers/simple-mcp-server/.claude/commands/build.md @@ -0,0 +1,377 @@ +# Build MCP Server for Production + +Builds and prepares your MCP server for production deployment. + +## Usage + +``` +/build [target] [options] +``` + +## Targets + +- `node` - Build for Node.js (default) +- `docker` - Build Docker image +- `npm` - Prepare for npm publishing + +## Options + +- `--minify` - Minify output +- `--sourcemap` - Include source maps +- `--analyze` - Analyze bundle size + +## Implementation + +```typescript +import { exec } from 'child_process'; +import { promisify } from 'util'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +const execAsync = promisify(exec); + +async function buildServer( + target: 'node' | 'docker' | 'npm' = 'node', + options: { + minify?: boolean; + sourcemap?: boolean; + analyze?: boolean; + } = {} +) { + console.log('๐Ÿ”จ Building MCP Server for Production'); + console.log('='.repeat(50)); + + // Pre-build checks + await runPreBuildChecks(); + + // Build based on target + switch (target) { + case 'node': + await buildForNode(options); + break; + case 'docker': + await buildForDocker(options); + break; + case 'npm': + await buildForNpm(options); + break; + } + + // Post-build validation + await validateBuild(target); + + console.log('\nโœ… Build completed successfully!'); +} + +async function runPreBuildChecks() { + console.log('\n๐Ÿ” Running pre-build checks...'); + + // Check for uncommitted changes + try { + const { stdout: gitStatus } = await execAsync('git status --porcelain'); + if (gitStatus.trim()) { + console.warn('โš ๏ธ Warning: You have uncommitted changes'); + } + } catch { + // Git not available or not a git repo + } + + // Run tests + console.log(' Running tests...'); + try { + await execAsync('npm test'); + console.log(' โœ… Tests passed'); + } catch (error) { + console.error(' โŒ Tests failed'); + throw new Error('Build aborted: tests must pass'); + } + + // Check dependencies + console.log(' Checking dependencies...'); + try { + await execAsync('npm audit --production'); + console.log(' โœ… No vulnerabilities found'); + } catch (error) { + console.warn(' โš ๏ธ Security vulnerabilities detected'); + console.log(' Run "npm audit fix" to resolve'); + } +} + +async function buildForNode(options: any) { + console.log('\n๐Ÿ“ฆ Building for Node.js...'); + + // Clean previous build + await fs.rm('dist', { recursive: true, force: true }); + + // Update tsconfig for production + const tsConfig = JSON.parse(await fs.readFile('tsconfig.json', 'utf-8')); + const prodConfig = { + ...tsConfig, + compilerOptions: { + ...tsConfig.compilerOptions, + sourceMap: options.sourcemap || false, + inlineSources: false, + removeComments: true, + }, + }; + + await fs.writeFile('tsconfig.prod.json', JSON.stringify(prodConfig, null, 2)); + + // Build with TypeScript + console.log(' Compiling TypeScript...'); + await execAsync('npx tsc -p tsconfig.prod.json'); + + // Minify if requested + if (options.minify) { + console.log(' Minifying code...'); + await minifyCode(); + } + + // Copy package files + console.log(' Copying package files...'); + await fs.copyFile('package.json', 'dist/package.json'); + await fs.copyFile('README.md', 'dist/README.md').catch(() => {}); + await fs.copyFile('LICENSE', 'dist/LICENSE').catch(() => {}); + + // Create production package.json + const pkg = JSON.parse(await fs.readFile('package.json', 'utf-8')); + const prodPkg = { + ...pkg, + scripts: { + start: 'node index.js', + }, + devDependencies: undefined, + }; + await fs.writeFile('dist/package.json', JSON.stringify(prodPkg, null, 2)); + + // Analyze bundle if requested + if (options.analyze) { + await analyzeBundleSize(); + } + + console.log(' โœ… Node.js build complete'); + console.log(' Output: ./dist'); +} + +async function buildForDocker(options: any) { + console.log('\n๐Ÿ‹ Building Docker image...'); + + // Build Node.js first + await buildForNode(options); + + // Create Dockerfile if it doesn't exist + const dockerfilePath = 'Dockerfile'; + if (!await fs.access(dockerfilePath).then(() => true).catch(() => false)) { + await createDockerfile(); + } + + // Build Docker image + console.log(' Building Docker image...'); + const imageName = 'mcp-server'; + const version = JSON.parse(await fs.readFile('package.json', 'utf-8')).version; + + await execAsync(`docker build -t ${imageName}:${version} -t ${imageName}:latest .`); + + // Test the image + console.log(' Testing Docker image...'); + const { stdout } = await execAsync(`docker run --rm ${imageName}:latest node index.js --version`); + console.log(` Version: ${stdout.trim()}`); + + console.log(' โœ… Docker build complete'); + console.log(` Image: ${imageName}:${version}`); + console.log(' Run: docker run -it ' + imageName + ':latest'); +} + +async function buildForNpm(options: any) { + console.log('\n๐Ÿ“ฆ Preparing for npm publishing...'); + + // Build Node.js first + await buildForNode(options); + + // Validate package.json + const pkg = JSON.parse(await fs.readFile('package.json', 'utf-8')); + + if (!pkg.name) { + throw new Error('package.json must have a name field'); + } + + if (!pkg.version) { + throw new Error('package.json must have a version field'); + } + + if (!pkg.description) { + console.warn('โš ๏ธ Warning: package.json should have a description'); + } + + // Create .npmignore if needed + const npmignorePath = '.npmignore'; + if (!await fs.access(npmignorePath).then(() => true).catch(() => false)) { + await createNpmIgnore(); + } + + // Run npm pack to test + console.log(' Creating package tarball...'); + const { stdout } = await execAsync('npm pack --dry-run'); + + // Parse package contents + const files = stdout.split('\n').filter(line => line.includes('npm notice')); + console.log(` Package will include ${files.length} files`); + + // Check package size + const sizeMatch = stdout.match(/npm notice ([\d.]+[kMG]B)/); + if (sizeMatch) { + console.log(` Package size: ${sizeMatch[1]}`); + + // Warn if package is large + if (sizeMatch[1].includes('M') && parseFloat(sizeMatch[1]) > 10) { + console.warn('โš ๏ธ Warning: Package is larger than 10MB'); + } + } + + console.log(' โœ… npm package ready'); + console.log(' Publish with: npm publish'); +} + +async function createDockerfile() { + const dockerfile = ` +# Build stage +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# Production stage +FROM node:20-alpine +WORKDIR /app + +# Install dumb-init for signal handling +RUN apk add --no-cache dumb-init + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \\ + adduser -S nodejs -u 1001 + +# Copy built application +COPY --from=builder /app/dist ./ +COPY --from=builder /app/node_modules ./node_modules + +# Change ownership +RUN chown -R nodejs:nodejs /app + +USER nodejs + +EXPOSE 3000 + +ENTRYPOINT ["dumb-init", "--"] +CMD ["node", "index.js"] +`; + + await fs.writeFile('Dockerfile', dockerfile); + console.log(' Created Dockerfile'); +} + +async function createNpmIgnore() { + const npmignore = ` +# Source files +src/ +tests/ + +# Config files +.eslintrc* +.prettierrc* +tsconfig*.json +vitest.config.ts + +# Development files +*.log +.env +.env.* +!.env.example + +# Build artifacts +*.tsbuildinfo +coverage/ +.nyc_output/ + +# Docker +Dockerfile +docker-compose.yml + +# Git +.git/ +.gitignore + +# CI/CD +.github/ +.gitlab-ci.yml + +# IDE +.vscode/ +.idea/ + +# Misc +.DS_Store +Thumbs.db +`; + + await fs.writeFile('.npmignore', npmignore); + console.log(' Created .npmignore'); +} + +async function minifyCode() { + // Use esbuild or terser to minify + try { + await execAsync('npx esbuild dist/**/*.js --minify --outdir=dist --allow-overwrite'); + } catch { + console.warn(' Minification skipped (esbuild not available)'); + } +} + +async function analyzeBundleSize() { + console.log('\n๐Ÿ“Š Analyzing bundle size...'); + + const files = await fs.readdir('dist', { recursive: true }); + let totalSize = 0; + const fileSizes: Array<{ name: string; size: number }> = []; + + for (const file of files) { + const filePath = path.join('dist', file as string); + const stat = await fs.stat(filePath); + + if (stat.isFile()) { + totalSize += stat.size; + fileSizes.push({ name: file as string, size: stat.size }); + } + } + + // Sort by size + fileSizes.sort((a, b) => b.size - a.size); + + console.log(' Largest files:'); + fileSizes.slice(0, 5).forEach(file => { + console.log(` ${file.name}: ${(file.size / 1024).toFixed(2)}KB`); + }); + + console.log(` Total size: ${(totalSize / 1024).toFixed(2)}KB`); +} + +async function validateBuild(target: string) { + console.log('\n๐Ÿ” Validating build...'); + + // Check that main file exists + const mainFile = 'dist/index.js'; + if (!await fs.access(mainFile).then(() => true).catch(() => false)) { + throw new Error('Build validation failed: main file not found'); + } + + // Try to load the server + try { + await import(path.resolve(mainFile)); + console.log(' โœ… Server module loads successfully'); + } catch (error) { + throw new Error(`Build validation failed: ${error.message}`); + } +} +``` \ No newline at end of file diff --git a/mcp-servers/simple-mcp-server/.claude/commands/debug.md b/mcp-servers/simple-mcp-server/.claude/commands/debug.md new file mode 100644 index 0000000..47eca5f --- /dev/null +++ b/mcp-servers/simple-mcp-server/.claude/commands/debug.md @@ -0,0 +1,310 @@ +# Debug MCP Server + +Provides comprehensive debugging tools for troubleshooting MCP server issues. + +## Usage + +``` +/debug [component] [options] +``` + +## Components + +- `protocol` - Debug protocol messages +- `tools` - Debug tool execution +- `resources` - Debug resource access +- `transport` - Debug transport layer +- `all` - Enable all debugging (default) + +## Options + +- `--verbose` - Extra verbose output +- `--save` - Save debug logs to file +- `--inspector` - Launch with MCP Inspector + +## Implementation + +```typescript +import * as fs from 'fs/promises'; +import { exec, spawn } from 'child_process'; +import * as path from 'path'; + +async function debugServer( + component: 'protocol' | 'tools' | 'resources' | 'transport' | 'all' = 'all', + options: { + verbose?: boolean; + save?: boolean; + inspector?: boolean; + } = {} +) { + console.log('๐Ÿ” MCP Server Debugger'); + console.log('='.repeat(50)); + + // Set debug environment variables + const debugEnv = { + ...process.env, + DEBUG: component === 'all' ? 'mcp:*' : `mcp:${component}`, + LOG_LEVEL: options.verbose ? 'trace' : 'debug', + MCP_DEBUG: 'true', + }; + + // Create debug configuration + const debugConfig = await createDebugConfig(); + + // Start debug session + if (options.inspector) { + await launchWithInspector(debugEnv); + } else { + await runDebugSession(component, debugEnv, options); + } +} + +async function createDebugConfig(): Promise { + const config = { + logging: { + level: 'debug', + format: 'pretty', + includeTimestamp: true, + includeLocation: true, + }, + debug: { + protocol: { + logRequests: true, + logResponses: true, + logNotifications: true, + }, + tools: { + logCalls: true, + logValidation: true, + logErrors: true, + measurePerformance: true, + }, + resources: { + logReads: true, + logWrites: true, + trackCache: true, + }, + transport: { + logConnections: true, + logMessages: true, + logErrors: true, + }, + }, + }; + + const configPath = '.debug-config.json'; + await fs.writeFile(configPath, JSON.stringify(config, null, 2)); + return configPath; +} + +async function runDebugSession( + component: string, + env: NodeJS.ProcessEnv, + options: { verbose?: boolean; save?: boolean } +) { + console.log(`\n๐Ÿ” Debugging: ${component}`); + console.log('Press Ctrl+C to stop\n'); + + // Create debug wrapper + const debugScript = ` +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import debug from 'debug'; +import pino from 'pino'; + +// Enable debug logging +const log = { + protocol: debug('mcp:protocol'), + tools: debug('mcp:tools'), + resources: debug('mcp:resources'), + transport: debug('mcp:transport'), +}; + +// Create logger +const logger = pino({ + level: process.env.LOG_LEVEL || 'debug', + transport: { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'HH:MM:ss.l', + ignore: 'pid,hostname', + }, + }, +}); + +// Wrap server methods for debugging +const originalServer = await import('./src/index.js'); +const server = originalServer.server; + +// Intercept requests +const originalSetRequestHandler = server.setRequestHandler.bind(server); +server.setRequestHandler = (schema, handler) => { + const wrappedHandler = async (request) => { + const start = Date.now(); + log.protocol('โ†’ Request: %O', request); + logger.debug({ request }, 'Incoming request'); + + try { + const result = await handler(request); + const duration = Date.now() - start; + + log.protocol('โ† Response (%dms): %O', duration, result); + logger.debug({ result, duration }, 'Response sent'); + + return result; + } catch (error) { + log.protocol('โœ— Error: %O', error); + logger.error({ error }, 'Request failed'); + throw error; + } + }; + + return originalSetRequestHandler(schema, wrappedHandler); +}; + +// Start server with debugging +logger.info('Debug server starting...'); +const transport = new StdioServerTransport(); +await server.connect(transport); +logger.info('Debug server ready'); +`; + + // Write debug script + const debugScriptPath = '.debug-server.js'; + await fs.writeFile(debugScriptPath, debugScript); + + // Start server with debugging + const serverProcess = spawn('node', [debugScriptPath], { + env, + stdio: options.save ? 'pipe' : 'inherit', + }); + + if (options.save) { + const logFile = `debug-${component}-${Date.now()}.log`; + const logStream = await fs.open(logFile, 'w'); + + serverProcess.stdout?.pipe(logStream.createWriteStream()); + serverProcess.stderr?.pipe(logStream.createWriteStream()); + + console.log(`Saving debug output to: ${logFile}`); + } + + // Handle cleanup + process.on('SIGINT', () => { + serverProcess.kill(); + process.exit(); + }); + + serverProcess.on('exit', async () => { + // Cleanup + await fs.unlink(debugScriptPath).catch(() => {}); + await fs.unlink('.debug-config.json').catch(() => {}); + }); +} + +async function launchWithInspector(env: NodeJS.ProcessEnv) { + console.log('\n๐Ÿ” Launching with MCP Inspector...'); + console.log('This will provide an interactive debugging interface.\n'); + + // Start server in debug mode + const serverProcess = spawn('node', ['--inspect', 'dist/index.js'], { + env, + stdio: 'pipe', + }); + + // Parse debug port from output + serverProcess.stderr?.on('data', (data) => { + const output = data.toString(); + const match = output.match(/Debugger listening on ws:\/\/(.+):(\d+)/); + if (match) { + console.log(`๐Ÿ”— Node.js debugger: chrome://inspect`); + console.log(` Connect to: ${match[1]}:${match[2]}`); + } + }); + + // Wait a moment for server to start + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Launch MCP Inspector + console.log('\n๐Ÿ” Starting MCP Inspector...'); + const inspector = exec('npx @modelcontextprotocol/inspector'); + + inspector.stdout?.on('data', (data) => { + console.log(data.toString()); + }); + + // Cleanup on exit + process.on('SIGINT', () => { + serverProcess.kill(); + inspector.kill(); + process.exit(); + }); +} + +// Additional debug utilities +export async function analyzeProtocolFlow() { + console.log('\n๐Ÿ“Š Analyzing Protocol Flow...'); + + const checks = [ + { name: 'Initialization', test: testInitialization }, + { name: 'Capability Negotiation', test: testCapabilities }, + { name: 'Tool Discovery', test: testToolDiscovery }, + { name: 'Resource Listing', test: testResourceListing }, + { name: 'Error Handling', test: testErrorHandling }, + ]; + + for (const check of checks) { + try { + await check.test(); + console.log(` โœ… ${check.name}`); + } catch (error) { + console.log(` โŒ ${check.name}: ${error.message}`); + } + } +} + +async function testInitialization() { + // Test initialization flow + const { Server } = await import('@modelcontextprotocol/sdk/server/index.js'); + const server = new Server({ name: 'test', version: '1.0.0' }, {}); + if (!server) throw new Error('Server initialization failed'); +} + +async function testCapabilities() { + // Test capability declaration + const capabilities = { + tools: {}, + resources: {}, + prompts: {}, + }; + if (!capabilities.tools) throw new Error('Tools capability missing'); +} + +async function testToolDiscovery() { + // Test tool discovery + try { + const { tools } = await import('./src/tools/index.js'); + if (!Array.isArray(tools)) throw new Error('Tools not properly exported'); + } catch { + // Tools may not be implemented yet + } +} + +async function testResourceListing() { + // Test resource listing + try { + const { resources } = await import('./src/resources/index.js'); + if (!Array.isArray(resources)) throw new Error('Resources not properly exported'); + } catch { + // Resources may not be implemented yet + } +} + +async function testErrorHandling() { + // Test error handling + const { handleError } = await import('./src/utils/error-handler.js'); + const result = handleError(new Error('Test')); + if (!result.error) throw new Error('Error handler not working'); +} +``` \ No newline at end of file diff --git a/mcp-servers/simple-mcp-server/.claude/commands/deploy.md b/mcp-servers/simple-mcp-server/.claude/commands/deploy.md new file mode 100644 index 0000000..8b4049e --- /dev/null +++ b/mcp-servers/simple-mcp-server/.claude/commands/deploy.md @@ -0,0 +1,376 @@ +# Deploy MCP Server + +Deploys your MCP server to various platforms and registries. + +## Usage + +``` +/deploy [target] [options] +``` + +## Targets + +- `npm` - Publish to npm registry +- `docker` - Push to Docker registry +- `claude` - Register with Claude Code +- `github` - Create GitHub release + +## Options + +- `--tag` - Version tag (default: from package.json) +- `--registry` - Custom registry URL +- `--dry-run` - Test deployment without publishing + +## Implementation + +```typescript +import { exec } from 'child_process'; +import { promisify } from 'util'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +const execAsync = promisify(exec); + +async function deployServer( + target: 'npm' | 'docker' | 'claude' | 'github', + options: { + tag?: string; + registry?: string; + dryRun?: boolean; + } = {} +) { + console.log('๐Ÿš€ Deploying MCP Server'); + console.log('='.repeat(50)); + + // Get version info + const pkg = JSON.parse(await fs.readFile('package.json', 'utf-8')); + const version = options.tag || pkg.version; + + // Pre-deployment checks + await runPreDeploymentChecks(version); + + // Deploy based on target + switch (target) { + case 'npm': + await deployToNpm(pkg, version, options); + break; + case 'docker': + await deployToDocker(pkg, version, options); + break; + case 'claude': + await deployToClaude(pkg, version, options); + break; + case 'github': + await deployToGitHub(pkg, version, options); + break; + } + + console.log('\nโœ… Deployment completed successfully!'); +} + +async function runPreDeploymentChecks(version: string) { + console.log('\n๐Ÿ” Running pre-deployment checks...'); + + // Check git status + try { + const { stdout: status } = await execAsync('git status --porcelain'); + if (status.trim()) { + throw new Error('Working directory has uncommitted changes'); + } + console.log(' โœ… Working directory clean'); + } catch (error) { + if (error.message.includes('uncommitted')) { + throw error; + } + console.warn(' โš ๏ธ Git not available'); + } + + // Check if version tag exists + try { + await execAsync(`git rev-parse v${version}`); + console.log(` โœ… Version tag v${version} exists`); + } catch { + console.warn(` โš ๏ธ Version tag v${version} not found`); + console.log(' Create with: git tag v' + version); + } + + // Verify build exists + const buildExists = await fs.access('dist').then(() => true).catch(() => false); + if (!buildExists) { + throw new Error('Build not found. Run /build first'); + } + console.log(' โœ… Build found'); + + // Run tests + console.log(' Running tests...'); + try { + await execAsync('npm test'); + console.log(' โœ… Tests passed'); + } catch { + throw new Error('Tests must pass before deployment'); + } +} + +async function deployToNpm(pkg: any, version: string, options: any) { + console.log(`\n๐Ÿ“ฆ Deploying to npm (v${version})...`); + + // Check npm authentication + try { + await execAsync('npm whoami'); + console.log(' โœ… npm authenticated'); + } catch { + throw new Error('Not authenticated with npm. Run: npm login'); + } + + // Check if version already published + try { + const { stdout } = await execAsync(`npm view ${pkg.name}@${version}`); + if (stdout) { + throw new Error(`Version ${version} already published`); + } + } catch (error) { + if (error.message.includes('already published')) { + throw error; + } + // Version not published yet (good) + } + + // Update version if different + if (pkg.version !== version) { + console.log(` Updating version to ${version}...`); + await execAsync(`npm version ${version} --no-git-tag-version`); + } + + // Publish package + const publishCmd = options.dryRun + ? 'npm publish --dry-run' + : `npm publish ${options.registry ? `--registry ${options.registry}` : ''}`; + + console.log(' Publishing to npm...'); + const { stdout } = await execAsync(publishCmd); + + if (options.dryRun) { + console.log(' ๐Ÿงช Dry run complete (not published)'); + } else { + console.log(' โœ… Published to npm'); + console.log(` Install with: npm install ${pkg.name}`); + console.log(` View at: https://www.npmjs.com/package/${pkg.name}`); + } +} + +async function deployToDocker(pkg: any, version: string, options: any) { + console.log(`\n๐Ÿ‹ Deploying to Docker registry (v${version})...`); + + const imageName = pkg.name.replace('@', '').replace('/', '-'); + const registry = options.registry || 'docker.io'; + const fullImageName = `${registry}/${imageName}`; + + // Check Docker authentication + try { + await execAsync(`docker pull ${registry}/hello-world`); + console.log(' โœ… Docker authenticated'); + } catch { + console.warn(' โš ๏ธ May not be authenticated with Docker registry'); + } + + // Build image if not exists + try { + await execAsync(`docker image inspect ${imageName}:${version}`); + console.log(' โœ… Docker image exists'); + } catch { + console.log(' Building Docker image...'); + await execAsync(`docker build -t ${imageName}:${version} -t ${imageName}:latest .`); + } + + // Tag for registry + console.log(' Tagging image...'); + await execAsync(`docker tag ${imageName}:${version} ${fullImageName}:${version}`); + await execAsync(`docker tag ${imageName}:latest ${fullImageName}:latest`); + + // Push to registry + if (!options.dryRun) { + console.log(' Pushing to registry...'); + await execAsync(`docker push ${fullImageName}:${version}`); + await execAsync(`docker push ${fullImageName}:latest`); + console.log(' โœ… Pushed to Docker registry'); + console.log(` Pull with: docker pull ${fullImageName}:${version}`); + } else { + console.log(' ๐Ÿงช Dry run complete (not pushed)'); + } +} + +async function deployToClaude(pkg: any, version: string, options: any) { + console.log(`\n๐Ÿค– Registering with Claude Code...`); + + // Create Claude integration instructions + const instructions = ` +# Claude Code Integration + +## Installation + +### From npm +\`\`\`bash +claude mcp add ${pkg.name} -- npx ${pkg.name} +\`\`\` + +### From local build +\`\`\`bash +claude mcp add ${pkg.name} -- node ${path.resolve('dist/index.js')} +\`\`\` + +### With custom configuration +\`\`\`bash +claude mcp add ${pkg.name} \\ + --env LOG_LEVEL=info \\ + --env CUSTOM_CONFIG=/path/to/config \\ + -- npx ${pkg.name} +\`\`\` + +## Available Capabilities + +- Tools: ${await countTools()} +- Resources: ${await countResources()} +- Prompts: ${await countPrompts()} + +## Version: ${version} +`; + + // Save integration instructions + const instructionsPath = 'CLAUDE_INTEGRATION.md'; + await fs.writeFile(instructionsPath, instructions); + + console.log(' โœ… Integration instructions created'); + console.log(` View: ${instructionsPath}`); + + // Test local integration + if (!options.dryRun) { + console.log(' Testing local integration...'); + try { + const { stdout } = await execAsync('claude mcp list'); + if (stdout.includes(pkg.name)) { + console.log(' โœ… Already registered with Claude Code'); + } else { + console.log(' Register with:'); + console.log(` claude mcp add ${pkg.name} -- node ${path.resolve('dist/index.js')}`); + } + } catch { + console.log(' Claude Code CLI not available'); + } + } +} + +async function deployToGitHub(pkg: any, version: string, options: any) { + console.log(`\n๐Ÿ’™ Creating GitHub release (v${version})...`); + + // Check gh CLI + try { + await execAsync('gh --version'); + } catch { + throw new Error('GitHub CLI not installed. Install from: https://cli.github.com'); + } + + // Check authentication + try { + await execAsync('gh auth status'); + console.log(' โœ… GitHub authenticated'); + } catch { + throw new Error('Not authenticated with GitHub. Run: gh auth login'); + } + + // Create release notes + const releaseNotes = await generateReleaseNotes(version); + const notesPath = `.release-notes-${version}.md`; + await fs.writeFile(notesPath, releaseNotes); + + // Create release + if (!options.dryRun) { + const releaseCmd = `gh release create v${version} \\ + --title "v${version}" \\ + --notes-file ${notesPath} \\ + --generate-notes`; + + try { + await execAsync(releaseCmd); + console.log(' โœ… GitHub release created'); + + // Get release URL + const { stdout } = await execAsync(`gh release view v${version} --json url`); + const { url } = JSON.parse(stdout); + console.log(` View at: ${url}`); + } catch (error) { + if (error.message.includes('already exists')) { + console.log(' โ„น๏ธ Release already exists'); + } else { + throw error; + } + } + } else { + console.log(' ๐Ÿงช Dry run complete (release not created)'); + console.log(` Release notes saved to: ${notesPath}`); + } + + // Cleanup + await fs.unlink(notesPath).catch(() => {}); +} + +async function generateReleaseNotes(version: string): Promise { + const pkg = JSON.parse(await fs.readFile('package.json', 'utf-8')); + + return ` +# ${pkg.name} v${version} + +## ๐ŸŽ† What's New + +- [Add your changes here] + +## ๐Ÿ“ฆ Installation + +### npm +\`\`\`bash +npm install ${pkg.name}@${version} +\`\`\` + +### Claude Code +\`\`\`bash +claude mcp add ${pkg.name} -- npx ${pkg.name}@${version} +\`\`\` + +## ๐Ÿ“„ Documentation + +See [README.md](README.md) for usage instructions. + +## ๐Ÿ”ง MCP Capabilities + +- Tools: ${await countTools()} +- Resources: ${await countResources()} +- Prompts: ${await countPrompts()} +`; +} + +async function countTools(): Promise { + try { + const files = await fs.readdir('src/tools').catch(() => []); + return files.filter(f => f.endsWith('.ts') && f !== 'index.ts').length; + } catch { + return 0; + } +} + +async function countResources(): Promise { + try { + const files = await fs.readdir('src/resources').catch(() => []); + return files.filter(f => f.endsWith('.ts') && f !== 'index.ts').length; + } catch { + return 0; + } +} + +async function countPrompts(): Promise { + try { + const files = await fs.readdir('src/prompts').catch(() => []); + return files.filter(f => f.endsWith('.ts') && f !== 'index.ts').length; + } catch { + return 0; + } +} +``` \ No newline at end of file diff --git a/mcp-servers/simple-mcp-server/.claude/commands/init.md b/mcp-servers/simple-mcp-server/.claude/commands/init.md new file mode 100644 index 0000000..885af94 --- /dev/null +++ b/mcp-servers/simple-mcp-server/.claude/commands/init.md @@ -0,0 +1,178 @@ +# Initialize MCP Server Project + +Sets up a new MCP server project with the specified configuration level. + +## Usage + +``` +/init [basic|standard|full] +``` + +## Options + +- `basic` - Minimal server with one example tool +- `standard` - Server with tools and resources (default) +- `full` - Complete server with all capabilities + +## Implementation + +```typescript +async function initializeProject(level: 'basic' | 'standard' | 'full' = 'standard') { + // Create project structure + const dirs = [ + 'src', + 'src/tools', + 'src/resources', + 'src/prompts', + 'src/utils', + 'src/types', + 'tests', + 'tests/unit', + 'tests/integration', + ]; + + for (const dir of dirs) { + await fs.mkdir(dir, { recursive: true }); + } + + // Create package.json + const packageJson = { + name: 'mcp-server', + version: '1.0.0', + type: 'module', + scripts: { + 'dev': 'tsx watch src/index.ts', + 'build': 'tsc', + 'start': 'node dist/index.js', + 'test': 'vitest', + 'lint': 'eslint src', + 'typecheck': 'tsc --noEmit', + }, + dependencies: { + '@modelcontextprotocol/sdk': '^1.0.0', + 'zod': '^3.22.0', + }, + devDependencies: { + '@types/node': '^20.0.0', + 'typescript': '^5.0.0', + 'tsx': '^4.0.0', + 'vitest': '^1.0.0', + 'eslint': '^8.0.0', + }, + }; + + await fs.writeFile('package.json', JSON.stringify(packageJson, null, 2)); + + // Create tsconfig.json + const tsConfig = { + compilerOptions: { + target: 'ES2022', + module: 'NodeNext', + moduleResolution: 'NodeNext', + outDir: './dist', + rootDir: './src', + strict: true, + esModuleInterop: true, + skipLibCheck: true, + forceConsistentCasingInFileNames: true, + }, + include: ['src/**/*'], + exclude: ['node_modules', 'dist'], + }; + + await fs.writeFile('tsconfig.json', JSON.stringify(tsConfig, null, 2)); + + // Create main server file + let serverContent = ''; + + if (level === 'basic') { + serverContent = generateBasicServer(); + } else if (level === 'standard') { + serverContent = generateStandardServer(); + } else { + serverContent = generateFullServer(); + } + + await fs.writeFile('src/index.ts', serverContent); + + // Install dependencies + console.log('Installing dependencies...'); + await exec('npm install'); + + console.log('โœ… MCP server initialized successfully!'); + console.log('\nNext steps:'); + console.log('1. Run "npm run dev" to start development server'); + console.log('2. Use "/add-tool" to add custom tools'); + console.log('3. Test with MCP Inspector: npx @modelcontextprotocol/inspector'); +} + +function generateBasicServer(): string { + return ` +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { z } from 'zod'; + +const server = new Server({ + name: 'my-mcp-server', + version: '1.0.0', +}, { + capabilities: { + tools: {}, + }, +}); + +// List available tools +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: 'hello', + description: 'Say hello to someone', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Name to greet', + }, + }, + required: ['name'], + }, + }, + ], + }; +}); + +// Handle tool calls +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + if (name === 'hello') { + const validated = z.object({ + name: z.string(), + }).parse(args); + + return { + content: [ + { + type: 'text', + text: \`Hello, \${validated.name}!\`, + }, + ], + }; + } + + throw new Error(\`Unknown tool: \${name}\`); +}); + +// Start server +const transport = new StdioServerTransport(); +await server.connect(transport); +console.error('MCP server running on stdio'); +`; +} +``` \ No newline at end of file diff --git a/mcp-servers/simple-mcp-server/.claude/commands/test.md b/mcp-servers/simple-mcp-server/.claude/commands/test.md new file mode 100644 index 0000000..eff8fe9 --- /dev/null +++ b/mcp-servers/simple-mcp-server/.claude/commands/test.md @@ -0,0 +1,261 @@ +# Test MCP Server + +Runs comprehensive tests for your MCP server including unit tests, integration tests, and protocol compliance validation. + +## Usage + +``` +/test [type] [options] +``` + +## Options + +- `type` - Test type: `unit`, `integration`, `all` (default: `all`) +- `--coverage` - Generate coverage report +- `--watch` - Run tests in watch mode +- `--inspector` - Launch MCP Inspector for manual testing + +## Implementation + +```typescript +import { exec } from 'child_process'; +import { promisify } from 'util'; +import * as fs from 'fs/promises'; + +const execAsync = promisify(exec); + +async function runTests( + type: 'unit' | 'integration' | 'all' = 'all', + options: { + coverage?: boolean; + watch?: boolean; + inspector?: boolean; + } = {} +) { + console.log('๐Ÿงช Running MCP Server Tests...'); + + // Run linting first + console.log('\n๐Ÿ” Running linter...'); + try { + await execAsync('npm run lint'); + console.log('โœ… Linting passed'); + } catch (error) { + console.error('โŒ Linting failed:', error.message); + return; + } + + // Run type checking + console.log('\n๐Ÿ“ Type checking...'); + try { + await execAsync('npm run typecheck'); + console.log('โœ… Type checking passed'); + } catch (error) { + console.error('โŒ Type checking failed:', error.message); + return; + } + + // Run tests + console.log(`\n๐Ÿงช Running ${type} tests...`); + + let testCommand = 'npx vitest'; + + if (type === 'unit') { + testCommand += ' tests/unit'; + } else if (type === 'integration') { + testCommand += ' tests/integration'; + } + + if (options.coverage) { + testCommand += ' --coverage'; + } + + if (options.watch) { + testCommand += ' --watch'; + } else { + testCommand += ' --run'; + } + + try { + const { stdout } = await execAsync(testCommand); + console.log(stdout); + + // Run protocol compliance check + if (type === 'all' || type === 'integration') { + console.log('\n๐Ÿ”Œ Checking MCP protocol compliance...'); + await checkProtocolCompliance(); + } + + // Generate test report + if (options.coverage) { + console.log('\n๐Ÿ“Š Coverage report generated:'); + console.log(' - HTML: coverage/index.html'); + console.log(' - JSON: coverage/coverage-final.json'); + } + + console.log('\nโœ… All tests passed!'); + + // Launch inspector if requested + if (options.inspector) { + console.log('\n๐Ÿ” Launching MCP Inspector...'); + await launchInspector(); + } + } catch (error) { + console.error('\nโŒ Tests failed:', error.message); + process.exit(1); + } +} + +async function checkProtocolCompliance() { + const tests = [ + checkInitialization, + checkToolsCapability, + checkResourcesCapability, + checkPromptsCapability, + checkErrorHandling, + ]; + + for (const test of tests) { + try { + await test(); + console.log(` โœ… ${test.name.replace('check', '')} compliance`); + } catch (error) { + console.log(` โŒ ${test.name.replace('check', '')} compliance: ${error.message}`); + throw error; + } + } +} + +async function checkInitialization() { + // Test that server properly handles initialization + const { Server } = await import('@modelcontextprotocol/sdk/server/index.js'); + const { TestTransport } = await import('../tests/utils/test-transport.js'); + + const server = new Server({ + name: 'test-server', + version: '1.0.0', + }, { + capabilities: { + tools: {}, + resources: {}, + prompts: {}, + }, + }); + + const transport = new TestTransport(); + await server.connect(transport); + + const response = await transport.request({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { + name: 'test-client', + version: '1.0.0', + }, + }, + }); + + if (!response.protocolVersion) { + throw new Error('Server did not return protocol version'); + } + + await server.close(); +} + +async function checkToolsCapability() { + // Verify tools capability is properly implemented + const toolsExist = await fs.access('src/tools') + .then(() => true) + .catch(() => false); + + if (!toolsExist) { + console.log(' (No tools implemented yet)'); + return; + } + + // Check that tools are properly exported + const { tools } = await import('../src/tools/index.js'); + if (!Array.isArray(tools)) { + throw new Error('Tools must be exported as an array'); + } +} + +async function checkResourcesCapability() { + // Verify resources capability is properly implemented + const resourcesExist = await fs.access('src/resources') + .then(() => true) + .catch(() => false); + + if (!resourcesExist) { + console.log(' (No resources implemented yet)'); + return; + } + + // Check that resources are properly exported + const { resources } = await import('../src/resources/index.js'); + if (!Array.isArray(resources)) { + throw new Error('Resources must be exported as an array'); + } +} + +async function checkPromptsCapability() { + // Verify prompts capability is properly implemented + const promptsExist = await fs.access('src/prompts') + .then(() => true) + .catch(() => false); + + if (!promptsExist) { + console.log(' (No prompts implemented yet)'); + return; + } + + // Check that prompts are properly exported + const { prompts } = await import('../src/prompts/index.js'); + if (!Array.isArray(prompts)) { + throw new Error('Prompts must be exported as an array'); + } +} + +async function checkErrorHandling() { + // Test that server properly handles errors + const { handleError } = await import('../src/utils/error-handler.js'); + + // Test known error + const knownError = new Error('Test error'); + knownError.code = 'TEST_ERROR'; + const response1 = handleError(knownError); + if (!response1.error || response1.error.code !== 'TEST_ERROR') { + throw new Error('Error handler does not preserve error codes'); + } + + // Test unknown error + const unknownError = new Error('Unknown error'); + const response2 = handleError(unknownError); + if (!response2.error || response2.error.code !== 'INTERNAL_ERROR') { + throw new Error('Error handler does not handle unknown errors'); + } +} + +async function launchInspector() { + console.log('Starting MCP Inspector...'); + console.log('This will open an interactive testing UI in your browser.'); + console.log('Press Ctrl+C to stop the inspector.\n'); + + const inspector = exec('npx @modelcontextprotocol/inspector'); + + inspector.stdout.on('data', (data) => { + console.log(data.toString()); + }); + + inspector.stderr.on('data', (data) => { + console.error(data.toString()); + }); + + inspector.on('close', (code) => { + console.log(`Inspector exited with code ${code}`); + }); +} +``` \ No newline at end of file -- cgit v1.2.3