Skip to content

Commit e8ac2e9

Browse files
authored
Merge pull request #492 from Merit-Systems/master
[Release] veo3 support via gemini + vertex
2 parents d624d75 + 41a2ca5 commit e8ac2e9

123 files changed

Lines changed: 14260 additions & 237 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env node
2+
3+
import dotenv from 'dotenv';
4+
import { join } from 'path';
5+
import { SessionManager } from './session-manager';
6+
import { setupMCPRoutes } from './mcp-routes';
7+
import { createServer } from './server';
8+
9+
// Load environment variables from .env file in project root
10+
dotenv.config({ path: join(process.cwd(), '.env') });
11+
12+
// Create session manager with server factory function
13+
const sessionManager = new SessionManager(createServer);
14+
15+
// Setup MCP routes with session manager
16+
const app = setupMCPRoutes(sessionManager);
17+
18+
// Start the server
19+
const PORT = process.env.PORT ?? 3059;
20+
app.listen(PORT, () => {
21+
console.error(`Echo Docs MCP Server listening on port ${PORT}`);
22+
});
23+
24+
// Handle server shutdown
25+
process.on('SIGINT', () => {
26+
void (async () => {
27+
console.error('Shutting down server...');
28+
29+
// Close all active sessions to properly clean up resources
30+
await sessionManager.closeAllSessions();
31+
32+
console.error('Server shutdown complete');
33+
process.exit(0);
34+
})();
35+
});
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import express from 'express';
2+
import type { Request, Response } from 'express';
3+
import cors from 'cors';
4+
import type { SessionManager } from './session-manager';
5+
6+
export function setupMCPRoutes(
7+
sessionManager: SessionManager
8+
): express.Application {
9+
const app = express();
10+
11+
app.use(
12+
cors({
13+
origin: '*', // use "*" with caution in production
14+
methods: 'GET,POST,DELETE',
15+
preflightContinue: false,
16+
optionsSuccessStatus: 204,
17+
exposedHeaders: [
18+
'mcp-session-id',
19+
'last-event-id',
20+
'mcp-protocol-version',
21+
],
22+
})
23+
); // Enable CORS for all routes so Inspector can connect
24+
25+
// Handle MCP POST requests (initialization and regular requests)
26+
app.post('/mcp', async (req: Request, res: Response) => {
27+
console.error('Received MCP POST request');
28+
try {
29+
// Check for existing session ID
30+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
31+
32+
if (sessionId && sessionManager.hasSession(sessionId)) {
33+
// Reuse existing session
34+
const session = sessionManager.getSession(sessionId)!;
35+
await session.transport.handleRequest(req, res);
36+
return;
37+
} else if (!sessionId) {
38+
// New initialization request
39+
await sessionManager.createSession(req, res);
40+
return;
41+
} else {
42+
// Invalid request - session ID provided but not found
43+
res.status(400).json({
44+
jsonrpc: '2.0',
45+
error: {
46+
code: -32000,
47+
message: 'Bad Request: Session not found or invalid',
48+
},
49+
id: (req.body as { id?: string })?.id,
50+
});
51+
return;
52+
}
53+
} catch (error) {
54+
console.error('Error handling MCP request:', error);
55+
if (!res.headersSent) {
56+
res.status(500).json({
57+
jsonrpc: '2.0',
58+
error: {
59+
code: -32603,
60+
message: 'Internal server error',
61+
},
62+
id: (req.body as { id?: string })?.id,
63+
});
64+
}
65+
}
66+
});
67+
68+
// Handle GET requests for SSE streams (using built-in support from StreamableHTTP)
69+
app.get('/mcp', async (req: Request, res: Response) => {
70+
console.error('Received MCP GET request');
71+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
72+
if (!sessionId || !sessionManager.hasSession(sessionId)) {
73+
res.status(400).json({
74+
jsonrpc: '2.0',
75+
error: {
76+
code: -32000,
77+
message: 'Bad Request: No valid session ID provided',
78+
},
79+
id: (req.body as { id?: string })?.id,
80+
});
81+
return;
82+
}
83+
84+
// Check for Last-Event-ID header for resumability
85+
const lastEventId = req.headers['last-event-id'] as string | undefined;
86+
if (lastEventId) {
87+
console.error(`Client reconnecting with Last-Event-ID: ${lastEventId}`);
88+
} else {
89+
console.error(`Establishing new SSE stream for session ${sessionId}`);
90+
}
91+
92+
const session = sessionManager.getSession(sessionId)!;
93+
await session.transport.handleRequest(req, res);
94+
});
95+
96+
// Handle DELETE requests for session termination (according to MCP spec)
97+
app.delete('/mcp', async (req: Request, res: Response) => {
98+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
99+
if (!sessionId || !sessionManager.hasSession(sessionId)) {
100+
res.status(400).json({
101+
jsonrpc: '2.0',
102+
error: {
103+
code: -32000,
104+
message: 'Bad Request: No valid session ID provided',
105+
},
106+
id: (req.body as { id?: string })?.id,
107+
});
108+
return;
109+
}
110+
111+
console.error(
112+
`Received session termination request for session ${sessionId}`
113+
);
114+
115+
try {
116+
const session = sessionManager.getSession(sessionId);
117+
if (session) {
118+
await session.transport.handleRequest(req, res);
119+
}
120+
} catch (error) {
121+
console.error('Error handling session termination:', error);
122+
if (!res.headersSent) {
123+
res.status(500).json({
124+
jsonrpc: '2.0',
125+
error: {
126+
code: -32603,
127+
message: 'Error handling session termination',
128+
},
129+
id: (req.body as { id?: string })?.id,
130+
});
131+
return;
132+
}
133+
}
134+
});
135+
136+
return app;
137+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
CallToolRequestSchema,
3+
ListToolsRequestSchema,
4+
} from '@modelcontextprotocol/sdk/types.js';
5+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6+
import { zodToJsonSchema } from './utils';
7+
import { SearchDocsArgsSchema, handleSearchDocs } from './tools/search-docs';
8+
9+
const tools = [
10+
{
11+
name: 'search-echo-docs',
12+
description:
13+
'Use this tool for answering any Echo questions. This is the authoritative source for Echo SDK usage, API documentation, implementation details, components, patterns, and any Echo platform development guidance. Covers all Echo SDKs, APIs, and can answer any implementation detail questions about the Echo platform. Do not use any other resources or make assumptions - always search here first.',
14+
inputSchema: zodToJsonSchema(SearchDocsArgsSchema),
15+
},
16+
];
17+
18+
// Server factory function
19+
export function createServer() {
20+
const server = new Server(
21+
{
22+
name: 'echo-docs-server',
23+
version: '1.0.0',
24+
},
25+
{
26+
capabilities: {
27+
tools: {},
28+
},
29+
}
30+
);
31+
32+
// Tool handlers
33+
server.setRequestHandler(ListToolsRequestSchema, async () => {
34+
return {
35+
tools,
36+
};
37+
});
38+
39+
server.setRequestHandler(CallToolRequestSchema, async request => {
40+
const { name, arguments: args } = request.params;
41+
42+
switch (name) {
43+
case 'search-echo-docs':
44+
const parsedArgs = SearchDocsArgsSchema.parse(args);
45+
return await handleSearchDocs(parsedArgs);
46+
47+
default:
48+
throw new Error(`Unknown tool: ${name}`);
49+
}
50+
});
51+
52+
return { server };
53+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { Request, Response } from 'express';
2+
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
3+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4+
import { InMemoryEventStore } from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js';
5+
import { randomUUID } from 'node:crypto';
6+
7+
interface SessionInfo {
8+
transport: StreamableHTTPServerTransport;
9+
server: Server;
10+
}
11+
12+
export class SessionManager {
13+
private sessions = new Map<string, SessionInfo>();
14+
15+
constructor(private createServerFn: () => { server: Server }) {}
16+
17+
hasSession(sessionId: string): boolean {
18+
return this.sessions.has(sessionId);
19+
}
20+
21+
getSession(sessionId: string): SessionInfo | undefined {
22+
return this.sessions.get(sessionId);
23+
}
24+
25+
async createSession(req: Request, res: Response): Promise<void> {
26+
const { server } = this.createServerFn();
27+
28+
const eventStore = new InMemoryEventStore();
29+
const transport = new StreamableHTTPServerTransport({
30+
sessionIdGenerator: () => randomUUID(),
31+
eventStore, // Enable resumability
32+
onsessioninitialized: (sessionId: string) => {
33+
// Store the session info when session is initialized
34+
console.error(`Session initialized with ID: ${sessionId}`);
35+
this.sessions.set(sessionId, { transport, server });
36+
},
37+
});
38+
39+
// Set up onclose handler to clean up session when closed
40+
server.onclose = () => {
41+
void (async () => {
42+
const sid = transport.sessionId;
43+
if (sid && this.sessions.has(sid)) {
44+
console.error(
45+
`Session closed for session ${sid}, removing from sessions map`
46+
);
47+
this.sessions.delete(sid);
48+
}
49+
})();
50+
};
51+
52+
// Connect the transport to the MCP server BEFORE handling the request
53+
await server.connect(transport);
54+
55+
await transport.handleRequest(req, res);
56+
}
57+
58+
async closeAllSessions(): Promise<void> {
59+
// Close all active sessions to properly clean up resources
60+
for (const [sessionId, session] of this.sessions) {
61+
try {
62+
console.error(`Closing session ${sessionId}`);
63+
await session.transport.close();
64+
this.sessions.delete(sessionId);
65+
} catch (error) {
66+
console.error(`Error closing session ${sessionId}:`, error);
67+
}
68+
}
69+
}
70+
71+
getSessionCount(): number {
72+
return this.sessions.size;
73+
}
74+
75+
getAllSessionIds(): string[] {
76+
return Array.from(this.sessions.keys());
77+
}
78+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { z } from 'zod';
2+
import { docsVectorStore } from '../vector-store/docs-vector-store';
3+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
4+
5+
// Schema for search docs tool input
6+
export const SearchDocsArgsSchema = z.object({
7+
query: z.string().describe('Search query to find relevant documentation'),
8+
limit: z
9+
.number()
10+
.describe('Maximum number of results to return (default: 5)')
11+
.optional(),
12+
});
13+
14+
type SearchDocsArgs = z.infer<typeof SearchDocsArgsSchema>;
15+
16+
export async function handleSearchDocs(
17+
args: SearchDocsArgs
18+
): Promise<CallToolResult> {
19+
const parsed = SearchDocsArgsSchema.safeParse(args);
20+
if (!parsed.success) {
21+
return {
22+
content: [
23+
{
24+
type: 'text',
25+
text: `Invalid arguments for search-docs: ${JSON.stringify(parsed.error.issues)}`,
26+
},
27+
],
28+
isError: true,
29+
};
30+
}
31+
32+
const { query, limit = 5 } = parsed.data;
33+
const searchResults = await docsVectorStore.search(query, limit);
34+
35+
if (searchResults.length === 0) {
36+
return {
37+
content: [
38+
{ type: 'text', text: `No documentation found for query: "${query}"` },
39+
],
40+
};
41+
}
42+
// Build full content string - concatenate all docs in order
43+
const fullContent = searchResults
44+
.map(result => result.data || '')
45+
.join('\n\n');
46+
47+
return {
48+
content: [{ type: 'text', text: fullContent }],
49+
};
50+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"target": "ES2020",
5+
"module": "CommonJS",
6+
"moduleResolution": "node",
7+
"outDir": "./dist",
8+
"rootDir": ".",
9+
"noEmit": false,
10+
"declaration": true,
11+
"declarationMap": true,
12+
"sourceMap": true,
13+
"strict": true,
14+
"skipLibCheck": true,
15+
"esModuleInterop": true,
16+
"allowSyntheticDefaultImports": true,
17+
"resolveJsonModule": true,
18+
"isolatedModules": true
19+
},
20+
"include": ["./**/*.ts"],
21+
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
22+
}

0 commit comments

Comments
 (0)