Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
468 changes: 468 additions & 0 deletions packages/mcp-server/README.md

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions packages/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@lytics/dev-agent-mcp",
"version": "0.1.0",
"description": "MCP server for dev-agent with extensible adapter framework",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"dev-agent-mcp": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"typecheck": "tsc --noEmit",
"lint": "biome lint ./src",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@lytics/dev-agent-core": "workspace:*",
"@lytics/dev-agent-subagents": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.2",
"vitest": "^2.1.8"
},
"files": [
"dist"
],
"publishConfig": {
"access": "public"
}
}

186 changes: 186 additions & 0 deletions packages/mcp-server/src/adapters/adapter-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/**
* Adapter Registry
* Manages adapter lifecycle and tool execution routing
*/

import { ErrorCode } from '../server/protocol/types';
import type { ToolAdapter } from './tool-adapter';
import type { AdapterContext, ToolDefinition, ToolExecutionContext, ToolResult } from './types';

export interface RegistryConfig {
autoDiscover?: boolean;
customAdaptersPath?: string;
}

export class AdapterRegistry {
private adapters = new Map<string, ToolAdapter>();
private context?: AdapterContext;
private config: RegistryConfig;

constructor(config: RegistryConfig = {}) {
this.config = config;
}

/**
* Register a single adapter
*/
register(adapter: ToolAdapter): void {
const toolName = adapter.getToolDefinition().name;

if (this.adapters.has(toolName)) {
throw new Error(`Adapter already registered: ${toolName}`);
}

this.adapters.set(toolName, adapter);
}

/**
* Unregister an adapter
*/
async unregister(toolName: string): Promise<void> {
const adapter = this.adapters.get(toolName);
if (!adapter) {
return;
}

// Call shutdown if available
if (adapter.shutdown) {
await adapter.shutdown();
}

this.adapters.delete(toolName);
}

/**
* Initialize all registered adapters
*/
async initializeAll(context: AdapterContext): Promise<void> {
this.context = context;

const initPromises = Array.from(this.adapters.values()).map((adapter) =>
adapter.initialize(context)
);

await Promise.all(initPromises);
}

/**
* Get all tool definitions (for MCP tools/list)
*/
getToolDefinitions(): ToolDefinition[] {
return Array.from(this.adapters.values()).map((adapter) => adapter.getToolDefinition());
}

/**
* Execute a tool by name
*/
async executeTool(
toolName: string,
args: Record<string, unknown>,
context: ToolExecutionContext
): Promise<ToolResult> {
const adapter = this.adapters.get(toolName);

if (!adapter) {
return {
success: false,
error: {
code: String(ErrorCode.ToolNotFound),
message: `Tool not found: ${toolName}`,
recoverable: false,
},
};
}

// Optional validation
if (adapter.validate) {
const validation = adapter.validate(args);
if (!validation.valid) {
return {
success: false,
error: {
code: String(ErrorCode.InvalidParams),
message: validation.error || 'Invalid arguments',
details: validation.details,
recoverable: true,
suggestion: 'Check the tool input schema and try again',
},
};
}
}

// Execute tool
try {
const startTime = Date.now();
const result = await adapter.execute(args, context);

// Add execution time if not present
if (result.success && result.metadata) {
result.metadata.executionTime = Date.now() - startTime;
}

return result;
} catch (error) {
context.logger.error('Tool execution failed', {
toolName,
error: error instanceof Error ? error.message : String(error),
});

return {
success: false,
error: {
code: String(ErrorCode.ToolExecutionError),
message: error instanceof Error ? error.message : 'Tool execution failed',
recoverable: true,
suggestion: 'Check the tool arguments and try again',
},
};
}
}

/**
* Get adapter by tool name
*/
getAdapter(toolName: string): ToolAdapter | undefined {
return this.adapters.get(toolName);
}

/**
* Get all registered tool names
*/
getToolNames(): string[] {
return Array.from(this.adapters.keys());
}

/**
* Check if a tool is registered
*/
hasTool(toolName: string): boolean {
return this.adapters.has(toolName);
}

/**
* Get registry statistics
*/
getStats(): {
totalAdapters: number;
toolNames: string[];
} {
return {
totalAdapters: this.adapters.size,
toolNames: this.getToolNames(),
};
}

/**
* Shutdown all adapters
*/
async shutdownAll(): Promise<void> {
const shutdownPromises = Array.from(this.adapters.values())
.filter((adapter) => adapter.shutdown)
.map((adapter) => adapter.shutdown!());

await Promise.all(shutdownPromises);
this.adapters.clear();
}
}
30 changes: 30 additions & 0 deletions packages/mcp-server/src/adapters/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Base Adapter Class
* All adapters (Tool, Resource, Prompt) extend from this
*/

import type { AdapterContext, AdapterMetadata } from './types';

export abstract class Adapter {
/**
* Adapter metadata (name, version, description)
*/
abstract readonly metadata: AdapterMetadata;

/**
* Initialize the adapter with context
* Called once when adapter is registered
*/
abstract initialize(context: AdapterContext): Promise<void>;

/**
* Optional: Cleanup when adapter is unregistered or server stops
*/
shutdown?(): Promise<void>;

/**
* Optional: Health check for adapter
* @returns true if healthy, false otherwise
*/
healthCheck?(): Promise<boolean>;
}
41 changes: 41 additions & 0 deletions packages/mcp-server/src/adapters/tool-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Tool Adapter Base Class
* Extends Adapter to provide tool-specific functionality
*/

import { Adapter } from './adapter';
import type { ToolDefinition, ToolExecutionContext, ToolResult, ValidationResult } from './types';

export abstract class ToolAdapter extends Adapter {
/**
* Get the tool definition (name, description, schema)
* This is used by MCP to list available tools
*/
abstract getToolDefinition(): ToolDefinition;

/**
* Execute the tool with given arguments
* @param args Tool arguments from MCP client
* @param context Execution context (logger, config, etc.)
* @returns Tool result (success/error with data)
*/
abstract execute(
args: Record<string, unknown>,
context: ToolExecutionContext
): Promise<ToolResult>;

/**
* Optional: Validate arguments before execution
* @param args Tool arguments to validate
* @returns Validation result
*/
validate?(args: Record<string, unknown>): ValidationResult;

/**
* Optional: Estimate token usage for the response
* Used for token budget management
* @param args Tool arguments
* @returns Estimated token count
*/
estimateTokens?(args: Record<string, unknown>): number;
}
69 changes: 69 additions & 0 deletions packages/mcp-server/src/adapters/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Adapter Framework Types
*/

import type { JSONSchema, ToolDefinition } from '../server/protocol/types';

// Adapter Metadata
export interface AdapterMetadata {
name: string;
version: string;
description: string;
author?: string;
}

// Adapter Context (provided during initialization)
export interface AdapterContext {
logger: Logger;
config: Config;
}

// Tool Execution Context (provided during tool execution)
export interface ToolExecutionContext extends AdapterContext {
userId?: string; // For multi-user scenarios
}

// Logger Interface
export interface Logger {
debug(message: string, meta?: Record<string, unknown>): void;
info(message: string, meta?: Record<string, unknown>): void;
warn(message: string, meta?: Record<string, unknown>): void;
error(message: string | Error, meta?: Record<string, unknown>): void;
}

// Config Interface
export interface Config {
repositoryPath: string;
[key: string]: unknown;
}

// Tool Result
export interface ToolResult {
success: boolean;
data?: unknown;
error?: AdapterError;
metadata?: {
tokenEstimate?: number;
executionTime?: number;
cached?: boolean;
};
}

// Adapter Error
export interface AdapterError {
code: string;
message: string;
details?: unknown;
recoverable?: boolean;
suggestion?: string;
}

// Validation Result
export interface ValidationResult {
valid: boolean;
error?: string;
details?: unknown;
}

// Re-export tool definition for convenience
export type { JSONSchema, ToolDefinition } from '../server/protocol/types';
25 changes: 25 additions & 0 deletions packages/mcp-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* MCP Server Entry Point
* Can be used as a standalone server or imported as a library
*/

// Adapter exports
export { Adapter } from './adapters/adapter';
export { AdapterRegistry, type RegistryConfig } from './adapters/adapter-registry';
export { ToolAdapter } from './adapters/tool-adapter';
export * from './adapters/types';
// Core exports
export { MCPServer, type MCPServerConfig } from './server/mcp-server';
// Protocol exports
export { JSONRPCHandler } from './server/protocol/jsonrpc';
export * from './server/protocol/types';
export { StdioTransport } from './server/transport/stdio-transport';
// Transport exports
export {
Transport,
type TransportConfig,
type TransportMessage,
} from './server/transport/transport';

// Utility exports
export { ConsoleLogger } from './utils/logger';
Loading