Claude Server MCP
by davidteren
Verified
- src
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import fs from 'fs/promises';
import path from 'path';
interface BaseContext {
id: string;
content: string;
timestamp: string;
tags?: string[];
metadata?: Record<string, unknown>;
}
interface ProjectContext extends BaseContext {
projectId: string;
parentContextId?: string;
references?: string[];
}
interface ConversationContext extends BaseContext {
sessionId: string;
continuationOf?: string;
}
type Context = ProjectContext | ConversationContext;
class ClaudeServer {
private server: Server;
private baseDir: string;
private contextsDir: string;
private projectsDir: string;
private indexFile: string;
constructor() {
this.server = new Server(
{
name: 'claude-server',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
// Use .claude directory in home for better organization
this.baseDir = path.join(process.env.HOME || '~', '.claude');
this.contextsDir = path.join(this.baseDir, 'contexts');
this.projectsDir = path.join(this.baseDir, 'projects');
this.indexFile = path.join(this.baseDir, 'context-index.json');
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private async ensureDirectories() {
await fs.mkdir(this.contextsDir, { recursive: true });
await fs.mkdir(this.projectsDir, { recursive: true });
}
private async getContextPath(id: string, projectId?: string): Promise<string> {
if (projectId) {
const projectDir = path.join(this.projectsDir, projectId);
await fs.mkdir(projectDir, { recursive: true });
return path.join(projectDir, `${id}.json`);
}
return path.join(this.contextsDir, `${id}.json`);
}
private async updateIndex(context: Context) {
try {
const indexData = await fs.readFile(this.indexFile, 'utf-8')
.then(data => JSON.parse(data))
.catch(() => ({ contexts: [] }));
const existingIndex = indexData.contexts.findIndex((c: Context) => c.id === context.id);
if (existingIndex >= 0) {
indexData.contexts[existingIndex] = context;
} else {
indexData.contexts.push(context);
}
await fs.writeFile(this.indexFile, JSON.stringify(indexData, null, 2), 'utf-8');
} catch (error) {
console.error('Error updating index:', error);
}
}
private async saveContext(context: Context) {
await this.ensureDirectories();
const contextPath = await this.getContextPath(
context.id,
'projectId' in context ? context.projectId : undefined
);
await fs.writeFile(contextPath, JSON.stringify(context, null, 2), 'utf-8');
await this.updateIndex(context);
}
private async getContext(id: string, projectId?: string): Promise<Context | null> {
try {
const contextPath = await this.getContextPath(id, projectId);
const data = await fs.readFile(contextPath, 'utf-8');
return JSON.parse(data);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
throw error;
}
}
private async listContexts(options: {
projectId?: string;
tag?: string;
type?: 'project' | 'conversation';
} = {}): Promise<Context[]> {
await this.ensureDirectories();
const getContextsFromDir = async (dir: string): Promise<Context[]> => {
const files = await fs.readdir(dir);
const contexts: Context[] = [];
for (const file of files) {
if (file.endsWith('.json')) {
const data = await fs.readFile(path.join(dir, file), 'utf-8');
contexts.push(JSON.parse(data));
}
}
return contexts;
};
let contexts: Context[] = [];
if (options.projectId) {
const projectDir = path.join(this.projectsDir, options.projectId);
contexts = await getContextsFromDir(projectDir);
} else if (options.type === 'project') {
contexts = await getContextsFromDir(this.projectsDir);
} else if (options.type === 'conversation') {
contexts = await getContextsFromDir(this.contextsDir);
} else {
const projectContexts = await getContextsFromDir(this.projectsDir);
const conversationContexts = await getContextsFromDir(this.contextsDir);
contexts = [...projectContexts, ...conversationContexts];
}
if (options.tag) {
contexts = contexts.filter(ctx => ctx.tags?.includes(options.tag!));
}
return contexts.sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'save_project_context',
description: 'Save project-specific context with relationships',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Unique identifier for the context',
},
projectId: {
type: 'string',
description: 'Project identifier',
},
content: {
type: 'string',
description: 'Context content to save',
},
parentContextId: {
type: 'string',
description: 'Optional ID of parent context',
},
references: {
type: 'array',
items: { type: 'string' },
description: 'Optional related context IDs',
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Optional tags for categorizing',
},
metadata: {
type: 'object',
description: 'Optional additional metadata',
},
},
required: ['id', 'projectId', 'content'],
},
},
{
name: 'save_conversation_context',
description: 'Save conversation context with continuation support',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Unique identifier for the context',
},
sessionId: {
type: 'string',
description: 'Conversation session identifier',
},
content: {
type: 'string',
description: 'Context content to save',
},
continuationOf: {
type: 'string',
description: 'Optional ID of previous context',
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Optional tags for categorizing',
},
metadata: {
type: 'object',
description: 'Optional additional metadata',
},
},
required: ['id', 'sessionId', 'content'],
},
},
{
name: 'get_context',
description: 'Retrieve context by ID and optional project ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'ID of the context to retrieve',
},
projectId: {
type: 'string',
description: 'Optional project ID for project contexts',
},
},
required: ['id'],
},
},
{
name: 'list_contexts',
description: 'List contexts with filtering options',
inputSchema: {
type: 'object',
properties: {
projectId: {
type: 'string',
description: 'Optional project ID to filter by',
},
tag: {
type: 'string',
description: 'Optional tag to filter by',
},
type: {
type: 'string',
enum: ['project', 'conversation'],
description: 'Optional type to filter by',
},
},
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case 'save_project_context': {
const {
id,
projectId,
content,
parentContextId,
references,
tags,
metadata,
} = request.params.arguments as {
id: string;
projectId: string;
content: string;
parentContextId?: string;
references?: string[];
tags?: string[];
metadata?: Record<string, unknown>;
};
const context: ProjectContext = {
id,
projectId,
content,
timestamp: new Date().toISOString(),
parentContextId,
references,
tags,
metadata,
};
await this.saveContext(context);
return {
content: [
{
type: 'text',
text: `Project context saved with ID: ${id}`,
},
],
};
}
case 'save_conversation_context': {
const {
id,
sessionId,
content,
continuationOf,
tags,
metadata,
} = request.params.arguments as {
id: string;
sessionId: string;
content: string;
continuationOf?: string;
tags?: string[];
metadata?: Record<string, unknown>;
};
const context: ConversationContext = {
id,
sessionId,
content,
timestamp: new Date().toISOString(),
continuationOf,
tags,
metadata,
};
await this.saveContext(context);
return {
content: [
{
type: 'text',
text: `Conversation context saved with ID: ${id}`,
},
],
};
}
case 'get_context': {
const { id, projectId } = request.params.arguments as {
id: string;
projectId?: string;
};
const context = await this.getContext(id, projectId);
if (!context) {
throw new McpError(
ErrorCode.InvalidRequest,
`Context not found with ID: ${id}`
);
}
return {
content: [
{
type: 'text',
text: context.content,
},
],
};
}
case 'list_contexts': {
const { projectId, tag, type } = request.params.arguments as {
projectId?: string;
tag?: string;
type?: 'project' | 'conversation';
};
const contexts = await this.listContexts({ projectId, tag, type });
return {
content: [
{
type: 'text',
text: JSON.stringify(contexts, null, 2),
},
],
};
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Claude MCP server running on stdio');
}
}
const server = new ClaudeServer();
server.run().catch(console.error);