Skip to content

Commit 50e6fcd

Browse files
committed
feat(mcp-server): implement MCP server core with adapter framework
- Add JSON-RPC 2.0 protocol handler - Implement stdio transport for process communication - Create extensible adapter framework (ToolAdapter, ResourceAdapter) - Build adapter registry with lifecycle management - Implement MCPServer core class with MCP protocol support - Add mock adapter for testing - Support tools/list, tools/call, and initialize methods Package structure: - src/server/protocol: JSON-RPC and MCP types - src/server/transport: Stdio transport implementation - src/adapters: Base adapter classes and registry - src/utils: Logger utilities - tests/adapters: Mock adapter for testing Relates to #27
1 parent 7a94bf1 commit 50e6fcd

File tree

15 files changed

+1685
-0
lines changed

15 files changed

+1685
-0
lines changed

packages/mcp-server/package.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "@lytics/dev-agent-mcp",
3+
"version": "0.1.0",
4+
"description": "MCP server for dev-agent with extensible adapter framework",
5+
"private": true,
6+
"main": "./dist/index.js",
7+
"types": "./dist/index.d.ts",
8+
"bin": {
9+
"dev-agent-mcp": "./dist/index.js"
10+
},
11+
"scripts": {
12+
"build": "tsc",
13+
"dev": "tsc --watch",
14+
"typecheck": "tsc --noEmit",
15+
"lint": "biome lint ./src",
16+
"test": "vitest run",
17+
"test:watch": "vitest"
18+
},
19+
"dependencies": {
20+
"@lytics/dev-agent-core": "workspace:*",
21+
"@lytics/dev-agent-subagents": "workspace:*"
22+
},
23+
"devDependencies": {
24+
"@types/node": "^22.0.0",
25+
"typescript": "^5.7.2",
26+
"vitest": "^2.1.8"
27+
},
28+
"files": [
29+
"dist"
30+
],
31+
"publishConfig": {
32+
"access": "public"
33+
}
34+
}
35+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Base Adapter Class
3+
* All adapters (Tool, Resource, Prompt) extend from this
4+
*/
5+
6+
import type { AdapterContext, AdapterMetadata } from './types';
7+
8+
export abstract class Adapter {
9+
/**
10+
* Adapter metadata (name, version, description)
11+
*/
12+
abstract readonly metadata: AdapterMetadata;
13+
14+
/**
15+
* Initialize the adapter with context
16+
* Called once when adapter is registered
17+
*/
18+
abstract initialize(context: AdapterContext): Promise<void>;
19+
20+
/**
21+
* Optional: Cleanup when adapter is unregistered or server stops
22+
*/
23+
shutdown?(): Promise<void>;
24+
25+
/**
26+
* Optional: Health check for adapter
27+
* @returns true if healthy, false otherwise
28+
*/
29+
healthCheck?(): Promise<boolean>;
30+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/**
2+
* Adapter Registry
3+
* Manages adapter lifecycle and tool execution routing
4+
*/
5+
6+
import { ErrorCode } from '../server/protocol/types';
7+
import type { ToolAdapter } from './ToolAdapter';
8+
import type { AdapterContext, ToolDefinition, ToolExecutionContext, ToolResult } from './types';
9+
10+
export interface RegistryConfig {
11+
autoDiscover?: boolean;
12+
customAdaptersPath?: string;
13+
}
14+
15+
export class AdapterRegistry {
16+
private adapters = new Map<string, ToolAdapter>();
17+
private context?: AdapterContext;
18+
private config: RegistryConfig;
19+
20+
constructor(config: RegistryConfig = {}) {
21+
this.config = config;
22+
}
23+
24+
/**
25+
* Register a single adapter
26+
*/
27+
register(adapter: ToolAdapter): void {
28+
const toolName = adapter.getToolDefinition().name;
29+
30+
if (this.adapters.has(toolName)) {
31+
throw new Error(`Adapter already registered: ${toolName}`);
32+
}
33+
34+
this.adapters.set(toolName, adapter);
35+
}
36+
37+
/**
38+
* Unregister an adapter
39+
*/
40+
async unregister(toolName: string): Promise<void> {
41+
const adapter = this.adapters.get(toolName);
42+
if (!adapter) {
43+
return;
44+
}
45+
46+
// Call shutdown if available
47+
if (adapter.shutdown) {
48+
await adapter.shutdown();
49+
}
50+
51+
this.adapters.delete(toolName);
52+
}
53+
54+
/**
55+
* Initialize all registered adapters
56+
*/
57+
async initializeAll(context: AdapterContext): Promise<void> {
58+
this.context = context;
59+
60+
const initPromises = Array.from(this.adapters.values()).map((adapter) =>
61+
adapter.initialize(context)
62+
);
63+
64+
await Promise.all(initPromises);
65+
}
66+
67+
/**
68+
* Get all tool definitions (for MCP tools/list)
69+
*/
70+
getToolDefinitions(): ToolDefinition[] {
71+
return Array.from(this.adapters.values()).map((adapter) => adapter.getToolDefinition());
72+
}
73+
74+
/**
75+
* Execute a tool by name
76+
*/
77+
async executeTool(
78+
toolName: string,
79+
args: Record<string, unknown>,
80+
context: ToolExecutionContext
81+
): Promise<ToolResult> {
82+
const adapter = this.adapters.get(toolName);
83+
84+
if (!adapter) {
85+
return {
86+
success: false,
87+
error: {
88+
code: String(ErrorCode.ToolNotFound),
89+
message: `Tool not found: ${toolName}`,
90+
recoverable: false,
91+
},
92+
};
93+
}
94+
95+
// Optional validation
96+
if (adapter.validate) {
97+
const validation = adapter.validate(args);
98+
if (!validation.valid) {
99+
return {
100+
success: false,
101+
error: {
102+
code: String(ErrorCode.InvalidParams),
103+
message: validation.error || 'Invalid arguments',
104+
details: validation.details,
105+
recoverable: true,
106+
suggestion: 'Check the tool input schema and try again',
107+
},
108+
};
109+
}
110+
}
111+
112+
// Execute tool
113+
try {
114+
const startTime = Date.now();
115+
const result = await adapter.execute(args, context);
116+
117+
// Add execution time if not present
118+
if (result.success && result.metadata) {
119+
result.metadata.executionTime = Date.now() - startTime;
120+
}
121+
122+
return result;
123+
} catch (error) {
124+
context.logger.error('Tool execution failed', {
125+
toolName,
126+
error: error instanceof Error ? error.message : String(error),
127+
});
128+
129+
return {
130+
success: false,
131+
error: {
132+
code: String(ErrorCode.ToolExecutionError),
133+
message: error instanceof Error ? error.message : 'Tool execution failed',
134+
recoverable: true,
135+
suggestion: 'Check the tool arguments and try again',
136+
},
137+
};
138+
}
139+
}
140+
141+
/**
142+
* Get adapter by tool name
143+
*/
144+
getAdapter(toolName: string): ToolAdapter | undefined {
145+
return this.adapters.get(toolName);
146+
}
147+
148+
/**
149+
* Get all registered tool names
150+
*/
151+
getToolNames(): string[] {
152+
return Array.from(this.adapters.keys());
153+
}
154+
155+
/**
156+
* Check if a tool is registered
157+
*/
158+
hasTool(toolName: string): boolean {
159+
return this.adapters.has(toolName);
160+
}
161+
162+
/**
163+
* Get registry statistics
164+
*/
165+
getStats(): {
166+
totalAdapters: number;
167+
toolNames: string[];
168+
} {
169+
return {
170+
totalAdapters: this.adapters.size,
171+
toolNames: this.getToolNames(),
172+
};
173+
}
174+
175+
/**
176+
* Shutdown all adapters
177+
*/
178+
async shutdownAll(): Promise<void> {
179+
const shutdownPromises = Array.from(this.adapters.values())
180+
.filter((adapter) => adapter.shutdown)
181+
.map((adapter) => adapter.shutdown!());
182+
183+
await Promise.all(shutdownPromises);
184+
this.adapters.clear();
185+
}
186+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Tool Adapter Base Class
3+
* Extends Adapter to provide tool-specific functionality
4+
*/
5+
6+
import { Adapter } from './Adapter';
7+
import type { ToolDefinition, ToolExecutionContext, ToolResult, ValidationResult } from './types';
8+
9+
export abstract class ToolAdapter extends Adapter {
10+
/**
11+
* Get the tool definition (name, description, schema)
12+
* This is used by MCP to list available tools
13+
*/
14+
abstract getToolDefinition(): ToolDefinition;
15+
16+
/**
17+
* Execute the tool with given arguments
18+
* @param args Tool arguments from MCP client
19+
* @param context Execution context (logger, config, etc.)
20+
* @returns Tool result (success/error with data)
21+
*/
22+
abstract execute(
23+
args: Record<string, unknown>,
24+
context: ToolExecutionContext
25+
): Promise<ToolResult>;
26+
27+
/**
28+
* Optional: Validate arguments before execution
29+
* @param args Tool arguments to validate
30+
* @returns Validation result
31+
*/
32+
validate?(args: Record<string, unknown>): ValidationResult;
33+
34+
/**
35+
* Optional: Estimate token usage for the response
36+
* Used for token budget management
37+
* @param args Tool arguments
38+
* @returns Estimated token count
39+
*/
40+
estimateTokens?(args: Record<string, unknown>): number;
41+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Adapter Framework Types
3+
*/
4+
5+
import type { JSONSchema, ToolDefinition } from '../server/protocol/types';
6+
7+
// Adapter Metadata
8+
export interface AdapterMetadata {
9+
name: string;
10+
version: string;
11+
description: string;
12+
author?: string;
13+
}
14+
15+
// Adapter Context (provided during initialization)
16+
export interface AdapterContext {
17+
logger: Logger;
18+
config: Config;
19+
}
20+
21+
// Tool Execution Context (provided during tool execution)
22+
export interface ToolExecutionContext extends AdapterContext {
23+
userId?: string; // For multi-user scenarios
24+
}
25+
26+
// Logger Interface
27+
export interface Logger {
28+
debug(message: string, meta?: Record<string, unknown>): void;
29+
info(message: string, meta?: Record<string, unknown>): void;
30+
warn(message: string, meta?: Record<string, unknown>): void;
31+
error(message: string | Error, meta?: Record<string, unknown>): void;
32+
}
33+
34+
// Config Interface
35+
export interface Config {
36+
repositoryPath: string;
37+
[key: string]: unknown;
38+
}
39+
40+
// Tool Result
41+
export interface ToolResult {
42+
success: boolean;
43+
data?: unknown;
44+
error?: AdapterError;
45+
metadata?: {
46+
tokenEstimate?: number;
47+
executionTime?: number;
48+
cached?: boolean;
49+
};
50+
}
51+
52+
// Adapter Error
53+
export interface AdapterError {
54+
code: string;
55+
message: string;
56+
details?: unknown;
57+
recoverable?: boolean;
58+
suggestion?: string;
59+
}
60+
61+
// Validation Result
62+
export interface ValidationResult {
63+
valid: boolean;
64+
error?: string;
65+
details?: unknown;
66+
}
67+
68+
// Re-export tool definition for convenience
69+
export type { JSONSchema, ToolDefinition } from '../server/protocol/types';

packages/mcp-server/src/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* MCP Server Entry Point
3+
* Can be used as a standalone server or imported as a library
4+
*/
5+
6+
// Adapter exports
7+
export { Adapter } from './adapters/Adapter';
8+
export { AdapterRegistry, type RegistryConfig } from './adapters/AdapterRegistry';
9+
export { ToolAdapter } from './adapters/ToolAdapter';
10+
export * from './adapters/types';
11+
// Core exports
12+
export { MCPServer, type MCPServerConfig } from './server/MCPServer';
13+
// Protocol exports
14+
export { JSONRPCHandler } from './server/protocol/jsonrpc';
15+
export * from './server/protocol/types';
16+
export { StdioTransport } from './server/transport/StdioTransport';
17+
// Transport exports
18+
export {
19+
Transport,
20+
type TransportConfig,
21+
type TransportMessage,
22+
} from './server/transport/Transport';
23+
24+
// Utility exports
25+
export { ConsoleLogger } from './utils/logger';

0 commit comments

Comments
 (0)