@mcp-ts/sdk 1.3.5 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/agui-adapter.d.mts +1 -1
- package/dist/adapters/agui-adapter.d.ts +1 -1
- package/dist/adapters/agui-adapter.js +2 -2
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs +2 -2
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- package/dist/adapters/agui-middleware.d.mts +1 -1
- package/dist/adapters/agui-middleware.d.ts +1 -1
- package/dist/adapters/agui-middleware.js.map +1 -1
- package/dist/adapters/agui-middleware.mjs.map +1 -1
- package/dist/adapters/ai-adapter.d.mts +1 -1
- package/dist/adapters/ai-adapter.d.ts +1 -1
- package/dist/adapters/ai-adapter.js +1 -1
- package/dist/adapters/ai-adapter.js.map +1 -1
- package/dist/adapters/ai-adapter.mjs +1 -1
- package/dist/adapters/ai-adapter.mjs.map +1 -1
- package/dist/adapters/langchain-adapter.d.mts +1 -1
- package/dist/adapters/langchain-adapter.d.ts +1 -1
- package/dist/adapters/langchain-adapter.js +1 -1
- package/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/langchain-adapter.mjs +1 -1
- package/dist/adapters/langchain-adapter.mjs.map +1 -1
- package/dist/adapters/mastra-adapter.d.mts +1 -1
- package/dist/adapters/mastra-adapter.d.ts +1 -1
- package/dist/adapters/mastra-adapter.js +1 -1
- package/dist/adapters/mastra-adapter.js.map +1 -1
- package/dist/adapters/mastra-adapter.mjs +1 -1
- package/dist/adapters/mastra-adapter.mjs.map +1 -1
- package/dist/bin/mcp-ts.d.mts +1 -0
- package/dist/bin/mcp-ts.d.ts +1 -0
- package/dist/bin/mcp-ts.js +105 -0
- package/dist/bin/mcp-ts.js.map +1 -0
- package/dist/bin/mcp-ts.mjs +82 -0
- package/dist/bin/mcp-ts.mjs.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +411 -90
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +350 -91
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-BYLarghq.d.ts → multi-session-client-CHE8QpVE.d.ts} +75 -5
- package/dist/{multi-session-client-CzhMkE0k.d.mts → multi-session-client-CQsRbxYI.d.mts} +75 -5
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +394 -90
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +350 -91
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.js +10 -2
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +10 -2
- package/dist/shared/index.mjs.map +1 -1
- package/package.json +19 -6
- package/src/adapters/agui-adapter.ts +222 -222
- package/src/adapters/ai-adapter.ts +115 -115
- package/src/adapters/langchain-adapter.ts +127 -127
- package/src/adapters/mastra-adapter.ts +126 -126
- package/src/bin/mcp-ts.ts +102 -0
- package/src/server/handlers/nextjs-handler.ts +12 -12
- package/src/server/handlers/sse-handler.ts +61 -61
- package/src/server/mcp/multi-session-client.ts +135 -39
- package/src/server/storage/file-backend.ts +4 -16
- package/src/server/storage/index.ts +68 -25
- package/src/server/storage/memory-backend.ts +7 -16
- package/src/server/storage/redis-backend.ts +12 -16
- package/src/server/storage/sqlite-backend.ts +3 -6
- package/src/server/storage/supabase-backend.ts +228 -0
- package/src/shared/event-routing.ts +28 -28
- package/src/shared/utils.ts +22 -0
- package/supabase/migrations/20260330195700_install_mcp_sessions.sql +84 -0
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
import { MCPClient } from '../server/mcp/oauth-client';
|
|
2
|
-
import { MultiSessionClient } from '../server/mcp/multi-session-client';
|
|
3
|
-
import type { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
export interface MastraAdapterOptions {
|
|
6
|
-
/**
|
|
7
|
-
* Prefix for tool names to avoid collision with other tools.
|
|
8
|
-
* Defaults to the client's serverId.
|
|
9
|
-
*/
|
|
10
|
-
prefix?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Interface definition for a Mastra tool since we might not have the SDK installed.
|
|
15
|
-
* Based on Mastra documentation.
|
|
16
|
-
*/
|
|
17
|
-
export interface MastraTool {
|
|
18
|
-
id: string;
|
|
19
|
-
description: string;
|
|
20
|
-
inputSchema: z.ZodType<any>;
|
|
21
|
-
execute: (args: any) => Promise<any>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Adapter to use MCP tools within Mastra agents.
|
|
26
|
-
*/
|
|
27
|
-
export class MastraAdapter {
|
|
28
|
-
private z: typeof z | undefined;
|
|
29
|
-
|
|
30
|
-
constructor(
|
|
31
|
-
private client: MCPClient | MultiSessionClient,
|
|
32
|
-
private options: MastraAdapterOptions = {}
|
|
33
|
-
) { }
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Lazy-loads Zod dependency
|
|
37
|
-
*/
|
|
38
|
-
private async ensureZod() {
|
|
39
|
-
if (!this.z) {
|
|
40
|
-
try {
|
|
41
|
-
const zod = await import('zod');
|
|
42
|
-
this.z = zod.z;
|
|
43
|
-
} catch (error) {
|
|
44
|
-
throw new Error(
|
|
45
|
-
'zod is not installed. Install with:\n' +
|
|
46
|
-
' npm install zod'
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
private async transformTools(client: MCPClient): Promise<Record<string, MastraTool>> {
|
|
55
|
-
if (!client.isConnected()) {
|
|
56
|
-
return {};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
await this.ensureZod();
|
|
60
|
-
|
|
61
|
-
const result = await client.listTools();
|
|
62
|
-
const prefix = this.options.prefix ?? client.getServerId() ?? 'mcp';
|
|
63
|
-
const tools: Record<string, MastraTool> = {};
|
|
64
|
-
|
|
65
|
-
for (const tool of result.tools) {
|
|
66
|
-
const toolName = `${prefix}_${tool.name}`;
|
|
67
|
-
|
|
68
|
-
// In a real implementation, you would use a library like 'json-schema-to-zod'
|
|
69
|
-
const schema = this.jsonSchemaToZod(tool.inputSchema);
|
|
70
|
-
|
|
71
|
-
tools[toolName] = {
|
|
72
|
-
id: toolName,
|
|
73
|
-
description: tool.description || `Tool ${tool.name}`,
|
|
74
|
-
inputSchema: schema,
|
|
75
|
-
execute: async (args: any) => {
|
|
76
|
-
return await client.callTool(tool.name, args);
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return tools;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private jsonSchemaToZod(schema: any): z.ZodType<any> {
|
|
85
|
-
try {
|
|
86
|
-
const { parseSchema } = require('json-schema-to-zod');
|
|
87
|
-
const zodSchemaString = parseSchema(schema);
|
|
88
|
-
// eslint-disable-next-line
|
|
89
|
-
return new Function('z', 'return ' + zodSchemaString)(this.z);
|
|
90
|
-
} catch (error) {
|
|
91
|
-
// Fallback: Accept any object if conversion fails
|
|
92
|
-
console.warn('[MastraAdapter] Failed to convert JSON Schema to Zod, using fallback:', error);
|
|
93
|
-
return this.z!.record(this.z!.any()).optional().describe("Dynamic Input");
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Fetches tools from the MCP server and converts them to Mastra tools.
|
|
99
|
-
*/
|
|
100
|
-
async getTools(): Promise<Record<string, MastraTool>> {
|
|
101
|
-
// Use duck typing instead of instanceof to handle module bundling issues
|
|
102
|
-
const isMultiSession = typeof (this.client as any).getClients === 'function';
|
|
103
|
-
const clients = isMultiSession
|
|
104
|
-
? (this.client as MultiSessionClient).getClients()
|
|
105
|
-
: [this.client as MCPClient];
|
|
106
|
-
|
|
107
|
-
const results = await Promise.all(
|
|
108
|
-
clients.map(async (client) => {
|
|
109
|
-
try {
|
|
110
|
-
return await this.transformTools(client);
|
|
111
|
-
} catch (error) {
|
|
112
|
-
console.error(`[MastraAdapter] Failed to fetch tools from ${client.getServerId()}:`, error);
|
|
113
|
-
return {};
|
|
114
|
-
}
|
|
115
|
-
})
|
|
116
|
-
);
|
|
117
|
-
return results.reduce((acc, tools) => ({ ...acc, ...tools }), {});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Convenience static method to fetch tools in a single line.
|
|
122
|
-
*/
|
|
123
|
-
static async getTools(client: MCPClient | MultiSessionClient, options: MastraAdapterOptions = {}): Promise<Record<string, MastraTool>> {
|
|
124
|
-
return new MastraAdapter(client, options).getTools();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
1
|
+
import { MCPClient } from '../server/mcp/oauth-client';
|
|
2
|
+
import { MultiSessionClient } from '../server/mcp/multi-session-client';
|
|
3
|
+
import type { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
export interface MastraAdapterOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Prefix for tool names to avoid collision with other tools.
|
|
8
|
+
* Defaults to the client's serverId.
|
|
9
|
+
*/
|
|
10
|
+
prefix?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Interface definition for a Mastra tool since we might not have the SDK installed.
|
|
15
|
+
* Based on Mastra documentation.
|
|
16
|
+
*/
|
|
17
|
+
export interface MastraTool {
|
|
18
|
+
id: string;
|
|
19
|
+
description: string;
|
|
20
|
+
inputSchema: z.ZodType<any>;
|
|
21
|
+
execute: (args: any) => Promise<any>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Adapter to use MCP tools within Mastra agents.
|
|
26
|
+
*/
|
|
27
|
+
export class MastraAdapter {
|
|
28
|
+
private z: typeof z | undefined;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private client: MCPClient | MultiSessionClient,
|
|
32
|
+
private options: MastraAdapterOptions = {}
|
|
33
|
+
) { }
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Lazy-loads Zod dependency
|
|
37
|
+
*/
|
|
38
|
+
private async ensureZod() {
|
|
39
|
+
if (!this.z) {
|
|
40
|
+
try {
|
|
41
|
+
const zod = await import('zod');
|
|
42
|
+
this.z = zod.z;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'zod is not installed. Install with:\n' +
|
|
46
|
+
' npm install zod'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
private async transformTools(client: MCPClient): Promise<Record<string, MastraTool>> {
|
|
55
|
+
if (!client.isConnected()) {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await this.ensureZod();
|
|
60
|
+
|
|
61
|
+
const result = await client.listTools();
|
|
62
|
+
const prefix = this.options.prefix ?? client.getServerId()?.replace(/-/g, '').substring(0, 8) ?? 'mcp';
|
|
63
|
+
const tools: Record<string, MastraTool> = {};
|
|
64
|
+
|
|
65
|
+
for (const tool of result.tools) {
|
|
66
|
+
const toolName = `${prefix}_${tool.name}`;
|
|
67
|
+
|
|
68
|
+
// In a real implementation, you would use a library like 'json-schema-to-zod'
|
|
69
|
+
const schema = this.jsonSchemaToZod(tool.inputSchema);
|
|
70
|
+
|
|
71
|
+
tools[toolName] = {
|
|
72
|
+
id: toolName,
|
|
73
|
+
description: tool.description || `Tool ${tool.name}`,
|
|
74
|
+
inputSchema: schema,
|
|
75
|
+
execute: async (args: any) => {
|
|
76
|
+
return await client.callTool(tool.name, args);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return tools;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private jsonSchemaToZod(schema: any): z.ZodType<any> {
|
|
85
|
+
try {
|
|
86
|
+
const { parseSchema } = require('json-schema-to-zod');
|
|
87
|
+
const zodSchemaString = parseSchema(schema);
|
|
88
|
+
// eslint-disable-next-line
|
|
89
|
+
return new Function('z', 'return ' + zodSchemaString)(this.z);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// Fallback: Accept any object if conversion fails
|
|
92
|
+
console.warn('[MastraAdapter] Failed to convert JSON Schema to Zod, using fallback:', error);
|
|
93
|
+
return this.z!.record(this.z!.any()).optional().describe("Dynamic Input");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Fetches tools from the MCP server and converts them to Mastra tools.
|
|
99
|
+
*/
|
|
100
|
+
async getTools(): Promise<Record<string, MastraTool>> {
|
|
101
|
+
// Use duck typing instead of instanceof to handle module bundling issues
|
|
102
|
+
const isMultiSession = typeof (this.client as any).getClients === 'function';
|
|
103
|
+
const clients = isMultiSession
|
|
104
|
+
? (this.client as MultiSessionClient).getClients()
|
|
105
|
+
: [this.client as MCPClient];
|
|
106
|
+
|
|
107
|
+
const results = await Promise.all(
|
|
108
|
+
clients.map(async (client) => {
|
|
109
|
+
try {
|
|
110
|
+
return await this.transformTools(client);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error(`[MastraAdapter] Failed to fetch tools from ${client.getServerId()}:`, error);
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
return results.reduce((acc, tools) => ({ ...acc, ...tools }), {});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Convenience static method to fetch tools in a single line.
|
|
122
|
+
*/
|
|
123
|
+
static async getTools(client: MCPClient | MultiSessionClient, options: MastraAdapterOptions = {}): Promise<Record<string, MastraTool>> {
|
|
124
|
+
return new MastraAdapter(client, options).getTools();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MCP-TS CLI Utility
|
|
7
|
+
*
|
|
8
|
+
* Provides helper commands for users of the @mcp-ts/sdk library.
|
|
9
|
+
*/
|
|
10
|
+
async function run() {
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const command = args[0];
|
|
13
|
+
|
|
14
|
+
if (command === 'supabase-init') {
|
|
15
|
+
await initSupabase();
|
|
16
|
+
} else {
|
|
17
|
+
showHelp();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function showHelp() {
|
|
22
|
+
console.log(`
|
|
23
|
+
🚀 MCP-TS CLI Utility
|
|
24
|
+
Usage: npx mcp-ts <command>
|
|
25
|
+
|
|
26
|
+
Commands:
|
|
27
|
+
supabase-init Initialize Supabase migrations in your project
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function initSupabase() {
|
|
32
|
+
console.log('🚀 Initializing Supabase storage for MCP-TS...');
|
|
33
|
+
|
|
34
|
+
// When running from dist/bin/mcp-ts.js (compiled), __dirname is dist/bin.
|
|
35
|
+
// The supabase/ migrations are at the root of the package.
|
|
36
|
+
// We need to look up two levels to find 'supabase' folder in the package.
|
|
37
|
+
const pkgRoot = path.resolve(__dirname, '../..');
|
|
38
|
+
const sourceDir = path.join(pkgRoot, 'supabase', 'migrations');
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(sourceDir)) {
|
|
41
|
+
console.error(`❌ Error: Could not find migration files in package at: ${sourceDir}`);
|
|
42
|
+
console.log('Please ensure you are running this from a project where @mcp-ts/sdk is installed.');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const targetDir = path.join(process.cwd(), 'supabase', 'migrations');
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
if (!fs.existsSync(targetDir)) {
|
|
50
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
51
|
+
console.log(`📁 Created directory: ${targetDir}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const files = fs.readdirSync(sourceDir);
|
|
55
|
+
let copiedCount = 0;
|
|
56
|
+
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
if (file.endsWith('.sql')) {
|
|
59
|
+
const srcPath = path.join(sourceDir, file);
|
|
60
|
+
const destPath = path.join(targetDir, file);
|
|
61
|
+
|
|
62
|
+
if (fs.existsSync(destPath)) {
|
|
63
|
+
console.log(`⏭️ Skipping existing migration: ${file}`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fs.copyFileSync(srcPath, destPath);
|
|
68
|
+
console.log(`✅ Copied: ${file}`);
|
|
69
|
+
copiedCount++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (copiedCount > 0) {
|
|
74
|
+
console.log('\n✨ Database migrations successfully initialized!');
|
|
75
|
+
console.log('\nNext steps:');
|
|
76
|
+
console.log('1. Link your Supabase project:');
|
|
77
|
+
console.log(' npx supabase link --project-ref <your-project-id>');
|
|
78
|
+
console.log('\n2. Push the migrations to your remote database:');
|
|
79
|
+
console.log(' npx supabase db push');
|
|
80
|
+
console.log('\n3. Add your Supabase credentials to .env:');
|
|
81
|
+
console.log(' SUPABASE_URL=https://<your-project-id>.supabase.co');
|
|
82
|
+
console.log(' SUPABASE_SERVICE_ROLE_KEY=<your-service-role-key>');
|
|
83
|
+
console.log('\n⚠️ Important: Use the service_role key (not the anon key) for server-side storage.');
|
|
84
|
+
console.log(' The service_role key bypasses RLS policies and is required for mcp-ts to work correctly.');
|
|
85
|
+
console.log(' Find it in: Supabase Dashboard -> Project Settings -> API -> service_role');
|
|
86
|
+
} else if (files.length > 0) {
|
|
87
|
+
console.log('\n👍 All migration files are already present in your project.');
|
|
88
|
+
console.log(' Ensure SUPABASE_SERVICE_ROLE_KEY (not SUPABASE_ANON_KEY) is set in your .env');
|
|
89
|
+
} else {
|
|
90
|
+
console.log('⚠️ No migration files found to copy.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
} catch (error: any) {
|
|
94
|
+
console.error(`❌ Error initializing Supabase: ${error.message}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
run().catch(err => {
|
|
100
|
+
console.error(err);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* - POST + JSON accepts direct RPC result response
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { SSEConnectionManager, type ClientMetadata } from './sse-handler.js';
|
|
9
|
-
import type { McpConnectionEvent, McpObservabilityEvent } from '../../shared/events.js';
|
|
10
|
-
import { isConnectionEvent, isRpcResponseEvent } from '../../shared/event-routing.js';
|
|
11
|
-
import type { McpRpcResponse } from '../../shared/types.js';
|
|
8
|
+
import { SSEConnectionManager, type ClientMetadata } from './sse-handler.js';
|
|
9
|
+
import type { McpConnectionEvent, McpObservabilityEvent } from '../../shared/events.js';
|
|
10
|
+
import { isConnectionEvent, isRpcResponseEvent } from '../../shared/event-routing.js';
|
|
11
|
+
import type { McpRpcResponse } from '../../shared/types.js';
|
|
12
12
|
|
|
13
13
|
export interface NextMcpHandlerOptions {
|
|
14
14
|
/**
|
|
@@ -139,14 +139,14 @@ export function createNextMcpHandler(options: NextMcpHandlerOptions = {}) {
|
|
|
139
139
|
|
|
140
140
|
const manager = new SSEConnectionManager(
|
|
141
141
|
toManagerOptions(identity, resolvedClientMetadata),
|
|
142
|
-
(event: McpConnectionEvent | McpObservabilityEvent | McpRpcResponse) => {
|
|
143
|
-
if (isRpcResponseEvent(event)) {
|
|
144
|
-
sendSSE('rpc-response', event);
|
|
145
|
-
} else if (isConnectionEvent(event)) {
|
|
146
|
-
sendSSE('connection', event);
|
|
147
|
-
} else {
|
|
148
|
-
sendSSE('observability', event);
|
|
149
|
-
}
|
|
142
|
+
(event: McpConnectionEvent | McpObservabilityEvent | McpRpcResponse) => {
|
|
143
|
+
if (isRpcResponseEvent(event)) {
|
|
144
|
+
sendSSE('rpc-response', event);
|
|
145
|
+
} else if (isConnectionEvent(event)) {
|
|
146
|
+
sendSSE('connection', event);
|
|
147
|
+
} else {
|
|
148
|
+
sendSSE('observability', event);
|
|
149
|
+
}
|
|
150
150
|
}
|
|
151
151
|
);
|
|
152
152
|
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { McpConnectionEvent, McpObservabilityEvent } from '../../shared/events.js';
|
|
16
|
-
import type {
|
|
17
|
-
McpRpcRequest,
|
|
16
|
+
import type {
|
|
17
|
+
McpRpcRequest,
|
|
18
18
|
McpRpcResponse,
|
|
19
19
|
ConnectParams,
|
|
20
20
|
DisconnectParams,
|
|
@@ -32,12 +32,12 @@ import type {
|
|
|
32
32
|
ListPromptsResult,
|
|
33
33
|
ListResourcesResult,
|
|
34
34
|
CallToolResult,
|
|
35
|
-
} from '../../shared/types.js';
|
|
36
|
-
import { RpcErrorCodes } from '../../shared/errors.js';
|
|
37
|
-
import { UnauthorizedError } from '../../shared/errors.js';
|
|
38
|
-
import { isConnectionEvent, isRpcResponseEvent } from '../../shared/event-routing.js';
|
|
39
|
-
import { MCPClient } from '../mcp/oauth-client.js';
|
|
40
|
-
import { storage } from '../storage/index.js';
|
|
35
|
+
} from '../../shared/types.js';
|
|
36
|
+
import { RpcErrorCodes } from '../../shared/errors.js';
|
|
37
|
+
import { UnauthorizedError } from '../../shared/errors.js';
|
|
38
|
+
import { isConnectionEvent, isRpcResponseEvent } from '../../shared/event-routing.js';
|
|
39
|
+
import { MCPClient } from '../mcp/oauth-client.js';
|
|
40
|
+
import { storage } from '../storage/index.js';
|
|
41
41
|
|
|
42
42
|
// ============================================
|
|
43
43
|
// Types & Interfaces
|
|
@@ -81,7 +81,7 @@ const DEFAULT_HEARTBEAT_INTERVAL = 30000;
|
|
|
81
81
|
* Manages a single SSE connection and handles MCP operations.
|
|
82
82
|
* Each instance corresponds to one connected browser client.
|
|
83
83
|
*/
|
|
84
|
-
export class SSEConnectionManager {
|
|
84
|
+
export class SSEConnectionManager {
|
|
85
85
|
private readonly identity: string;
|
|
86
86
|
private readonly clients = new Map<string, MCPClient>();
|
|
87
87
|
private heartbeatTimer?: NodeJS.Timeout;
|
|
@@ -236,7 +236,7 @@ export class SSEConnectionManager {
|
|
|
236
236
|
/**
|
|
237
237
|
* Connect to an MCP server
|
|
238
238
|
*/
|
|
239
|
-
private async connect(params: ConnectParams): Promise<ConnectResult> {
|
|
239
|
+
private async connect(params: ConnectParams): Promise<ConnectResult> {
|
|
240
240
|
const { serverName, serverUrl, callbackUrl, transportType } = params;
|
|
241
241
|
|
|
242
242
|
// Normalize serverId to max 12 chars to keep tool names under 64 chars (DeepSeek/OpenAI limits)
|
|
@@ -267,21 +267,21 @@ export class SSEConnectionManager {
|
|
|
267
267
|
// Generate session ID
|
|
268
268
|
const sessionId = await storage.generateSessionId();
|
|
269
269
|
|
|
270
|
-
try {
|
|
270
|
+
try {
|
|
271
271
|
// Get resolved client metadata
|
|
272
272
|
const clientMetadata = await this.getResolvedClientMetadata();
|
|
273
273
|
|
|
274
274
|
// Create MCP client
|
|
275
|
-
const client = new MCPClient({
|
|
276
|
-
identity: this.identity,
|
|
277
|
-
sessionId,
|
|
278
|
-
serverId,
|
|
279
|
-
serverName,
|
|
280
|
-
serverUrl,
|
|
281
|
-
callbackUrl,
|
|
282
|
-
transportType,
|
|
283
|
-
...clientMetadata, // Spread client metadata (clientName, clientUri, logoUri, policyUri)
|
|
284
|
-
});
|
|
275
|
+
const client = new MCPClient({
|
|
276
|
+
identity: this.identity,
|
|
277
|
+
sessionId,
|
|
278
|
+
serverId,
|
|
279
|
+
serverName,
|
|
280
|
+
serverUrl,
|
|
281
|
+
callbackUrl,
|
|
282
|
+
transportType,
|
|
283
|
+
...clientMetadata, // Spread client metadata (clientName, clientUri, logoUri, policyUri)
|
|
284
|
+
});
|
|
285
285
|
|
|
286
286
|
// Note: Session will be created by MCPClient after successful connection
|
|
287
287
|
// This ensures sessions only exist for successful or OAuth-pending connections
|
|
@@ -308,19 +308,19 @@ export class SSEConnectionManager {
|
|
|
308
308
|
sessionId,
|
|
309
309
|
success: true,
|
|
310
310
|
};
|
|
311
|
-
} catch (error) {
|
|
312
|
-
if (error instanceof UnauthorizedError) {
|
|
313
|
-
// OAuth-required is a pending-auth state, not a failed connection.
|
|
314
|
-
this.clients.delete(sessionId);
|
|
315
|
-
return {
|
|
316
|
-
sessionId,
|
|
317
|
-
success: true,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
this.emitConnectionEvent({
|
|
322
|
-
type: 'error',
|
|
323
|
-
sessionId,
|
|
311
|
+
} catch (error) {
|
|
312
|
+
if (error instanceof UnauthorizedError) {
|
|
313
|
+
// OAuth-required is a pending-auth state, not a failed connection.
|
|
314
|
+
this.clients.delete(sessionId);
|
|
315
|
+
return {
|
|
316
|
+
sessionId,
|
|
317
|
+
success: true,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.emitConnectionEvent({
|
|
322
|
+
type: 'error',
|
|
323
|
+
sessionId,
|
|
324
324
|
serverId,
|
|
325
325
|
error: error instanceof Error ? error.message : 'Connection failed',
|
|
326
326
|
errorType: 'connection',
|
|
@@ -466,18 +466,18 @@ export class SSEConnectionManager {
|
|
|
466
466
|
/**
|
|
467
467
|
* Complete OAuth authorization flow
|
|
468
468
|
*/
|
|
469
|
-
private async finishAuth(params: FinishAuthParams): Promise<FinishAuthResult> {
|
|
470
|
-
const { sessionId, code } = params;
|
|
471
|
-
|
|
472
|
-
const session = await storage.getSession(this.identity, sessionId);
|
|
473
|
-
if (!session) {
|
|
474
|
-
throw new Error('Session not found');
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
try {
|
|
478
|
-
const client = new MCPClient({
|
|
479
|
-
identity: this.identity,
|
|
480
|
-
sessionId,
|
|
469
|
+
private async finishAuth(params: FinishAuthParams): Promise<FinishAuthResult> {
|
|
470
|
+
const { sessionId, code } = params;
|
|
471
|
+
|
|
472
|
+
const session = await storage.getSession(this.identity, sessionId);
|
|
473
|
+
if (!session) {
|
|
474
|
+
throw new Error('Session not found');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
const client = new MCPClient({
|
|
479
|
+
identity: this.identity,
|
|
480
|
+
sessionId,
|
|
481
481
|
});
|
|
482
482
|
|
|
483
483
|
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
@@ -573,8 +573,8 @@ export class SSEConnectionManager {
|
|
|
573
573
|
* Create an SSE endpoint handler compatible with Node.js HTTP frameworks.
|
|
574
574
|
* Handles both SSE streaming (GET) and RPC requests (POST).
|
|
575
575
|
*/
|
|
576
|
-
export function createSSEHandler(options: SSEHandlerOptions) {
|
|
577
|
-
return async (req: { method?: string; on: Function }, res: { writeHead: Function; write: Function }) => {
|
|
576
|
+
export function createSSEHandler(options: SSEHandlerOptions) {
|
|
577
|
+
return async (req: { method?: string; on: Function }, res: { writeHead: Function; write: Function }) => {
|
|
578
578
|
// Set SSE headers
|
|
579
579
|
res.writeHead(200, {
|
|
580
580
|
'Content-Type': 'text/event-stream',
|
|
@@ -586,15 +586,15 @@ export function createSSEHandler(options: SSEHandlerOptions) {
|
|
|
586
586
|
// Send initial connection acknowledgment
|
|
587
587
|
writeSSEEvent(res, 'connected', { timestamp: Date.now() });
|
|
588
588
|
|
|
589
|
-
// Create connection manager with event routing
|
|
590
|
-
const manager = new SSEConnectionManager(options, (event) => {
|
|
591
|
-
if (isRpcResponseEvent(event)) {
|
|
592
|
-
writeSSEEvent(res, 'rpc-response', event);
|
|
593
|
-
} else if (isConnectionEvent(event)) {
|
|
594
|
-
writeSSEEvent(res, 'connection', event);
|
|
595
|
-
} else {
|
|
596
|
-
writeSSEEvent(res, 'observability', event);
|
|
597
|
-
}
|
|
589
|
+
// Create connection manager with event routing
|
|
590
|
+
const manager = new SSEConnectionManager(options, (event) => {
|
|
591
|
+
if (isRpcResponseEvent(event)) {
|
|
592
|
+
writeSSEEvent(res, 'rpc-response', event);
|
|
593
|
+
} else if (isConnectionEvent(event)) {
|
|
594
|
+
writeSSEEvent(res, 'connection', event);
|
|
595
|
+
} else {
|
|
596
|
+
writeSSEEvent(res, 'observability', event);
|
|
597
|
+
}
|
|
598
598
|
});
|
|
599
599
|
|
|
600
600
|
// Cleanup on client disconnect
|
|
@@ -625,7 +625,7 @@ export function createSSEHandler(options: SSEHandlerOptions) {
|
|
|
625
625
|
/**
|
|
626
626
|
* Write an SSE event to the response stream
|
|
627
627
|
*/
|
|
628
|
-
function writeSSEEvent(res: { write: Function }, event: string, data: unknown): void {
|
|
629
|
-
res.write(`event: ${event}\n`);
|
|
630
|
-
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
631
|
-
}
|
|
628
|
+
function writeSSEEvent(res: { write: Function }, event: string, data: unknown): void {
|
|
629
|
+
res.write(`event: ${event}\n`);
|
|
630
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
631
|
+
}
|