Skip to main content

Devlog Feb 1: MCP Implementation (Part 1)

· 5 min read
Eduardez
MoLOS Lead Developer

This week marks the beginning of MCP (Model Context Protocol) implementation. Part 1 focuses on the core server infrastructure, security layer, and protocol handling.

MCP Server Architecture

Official SDK Integration

We implemented the MCP server using the official MCP SDK for TypeScript:

// src/lib/server/mcp/mcp-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

export class McpServer {
private server: Server;

constructor() {
this.server = new Server(
{
name: 'molos-mcp',
version: '1.0.0'
},
{
capabilities: {
resources: {},
prompts: {},
tools: {}
}
}
);
}

async start() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
}

Note: After initial implementation, we moved from SSE (Server-Sent Events) to HTTP-only transport for better compatibility with HTTP-based clients.

JSON-RPC Utilities

The MCP protocol uses JSON-RPC for communication. We implemented utility functions for request/response handling:

// src/lib/server/mcp/json-rpc.ts
export interface JsonRpcRequest {
jsonrpc: '2.0';
method: string;
params?: unknown;
id: string | number;
}

export interface JsonRpcResponse {
jsonrpc: '2.0';
result?: unknown;
error?: {
code: number;
message: string;
data?: unknown;
};
id: string | number;
}

export function createJsonRpcResponse(
id: string | number,
result?: unknown,
error?: JsonRpcError
): JsonRpcResponse {
return {
jsonrpc: '2.0',
result,
error,
id
};
}

HTTP Transport Layer

We converted the transport to HTTP-only operation for broader client compatibility:

// src/routes/api/ai/mcp/transport/+server.ts
import type { RequestHandler } from './$types';
import { handleMcpRequest } from '$lib/server/mcp/handlers';
import { authenticateApiKey } from '$lib/server/mcp/auth';

export const POST: RequestHandler = async ({ request }) => {
try {
const apiKey = request.headers.get('x-api-key');
const session = await authenticateApiKey(apiKey);

if (!session) {
return error(401, 'Invalid API key');
}

const jsonRpcRequest = await request.json();
const result = await handleMcpRequest(jsonRpcRequest, session);

return json(result);
} catch (error) {
return error(500, 'Internal server error');
}
};

Request Validation

Zod Schema Validation

We use Zod for runtime validation of all incoming requests:

// src/lib/server/mcp/schemas.ts
import { z } from 'zod';

export const readResourceSchema = z.object({
uri: z.string().url(),
includeContext: z.boolean().optional().default(false)
});

export const listResourcesSchema = z.object({
cursor: z.string().optional()
});

export const getPromptSchema = z.object({
name: z.string(),
arguments: z.record(z.unknown()).optional()
});

Request Handler Pattern

Each MCP operation has a dedicated handler with validation:

// src/lib/server/mcp/handlers/resources.ts
import { readResourceSchema } from '../schemas';

export async function handleReadResource(
params: unknown,
session: McpSession
) {
const { uri, includeContext } = readResourceSchema.parse(params);

const resource = await findResource(uri);

if (!resource) {
throw new McpError(
ErrorCodes.InvalidRequest,
`Resource not found: ${uri}`
);
}

if (!hasResourceAccess(session, resource)) {
throw new McpError(
ErrorCodes.InvalidParams,
'Insufficient permissions for this resource'
);
}

const content = await readResourceContent(resource);

return {
contents: [{
uri,
mimeType: resource.mimeType,
text: content
}]
};
}

Security Layer

API Key Authentication

API keys authenticate requests and scope access to specific modules:

// src/lib/server/mcp/auth.ts
import { verifyApiKey } from './api-key-service';

export async function authenticateApiKey(
apiKey: string | null
): Promise<McpSession | null> {
if (!apiKey) {
return null;
}

const keyRecord = await verifyApiKey(apiKey);

if (!keyRecord || !keyRecord.isActive || keyRecord.expiresAt < new Date()) {
return null;
}

return {
keyId: keyRecord.id,
scopes: keyRecord.scopes,
modules: keyRecord.modules
};
}

Security Configuration

// src/lib/server/mcp/security.ts
export const securityConfig = {
maxRequestSize: 1024 * 1024, // 1MB
maxResponseSize: 10 * 1024 * 1024, // 10MB
enableRequestTimeout: true,
requestTimeoutMs: 30000,
requireHttps: !import.meta.env.DEV
};

Rate Limiting

Sliding Window Implementation

We implemented a sliding window rate limiter to prevent abuse:

// src/lib/server/mcp/rate-limit.ts
class RateLimiter {
private requests: Map<string, number[]> = new Map();

async checkLimit(keyId: string): Promise<boolean> {
const now = Date.now();
const windowStart = now - this.windowMs;
const timestamps = this.requests.get(keyId) || [];

const recentRequests = timestamps.filter(ts => ts > windowStart);

if (recentRequests.length >= this.maxRequests) {
return false;
}

recentRequests.push(now);
this.requests.set(keyId, recentRequests);
return true;
}
}

Rate limits are configured per API key:

  • Default: 100 requests per minute
  • Burst: 200 requests per minute
  • Configurable per key

Caching Layer

In-Memory Cache

We added a caching layer to improve performance for frequently accessed resources:

// src/lib/server/mcp/cache.ts
class McpCache {
private cache: Map<string, CacheEntry> = new Map();

get(key: string): unknown | undefined {
const entry = this.cache.get(key);

if (!entry || entry.expiresAt < Date.now()) {
this.cache.delete(key);
return undefined;
}

return entry.value;
}

set(key: string, value: unknown, ttlMs: number): void {
this.cache.set(key, {
value,
expiresAt: Date.now() + ttlMs
});
}
}

Cache TTL is configurable per resource type:

  • Static resources: 5 minutes
  • Dynamic resources: 1 minute
  • Real-time data: 30 seconds

Structured Logging

We added comprehensive logging for debugging and monitoring:

// src/lib/server/mcp/logger.ts
export class McpLogger {
private logQueue: LogEntry[] = [];

async log(level: LogLevel, message: string, meta?: unknown): Promise<void> {
const entry: LogEntry = {
timestamp: new Date(),
level,
message,
meta
};

this.logQueue.push(entry);

if (this.logQueue.length >= this.batchSize) {
await this.flush();
}
}

async flush(): Promise<void> {
if (this.logQueue.length === 0) return;

const entries = [...this.logQueue];
this.logQueue = [];

await writeToDatabase(entries);
}
}

Log levels include:

  • info: Normal operations
  • warn: Recoverable issues
  • error: Failures requiring attention
  • debug: Detailed troubleshooting info

View commits on GitHub

What's Next

Part 2 will cover:

  • MCP dashboard UI with Material Design 3
  • CRUD operations for resources and prompts
  • Activity log visualization
  • API key management UX improvements

Continue reading in Devlog Feb 5: MCP Implementation (Part 2).