@mcp-ts/sdk 1.3.4 → 1.3.5
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/README.md +404 -400
- package/dist/adapters/agui-adapter.d.mts +1 -1
- package/dist/adapters/agui-adapter.d.ts +1 -1
- package/dist/adapters/agui-middleware.d.mts +1 -1
- package/dist/adapters/agui-middleware.d.ts +1 -1
- package/dist/adapters/ai-adapter.d.mts +1 -1
- package/dist/adapters/ai-adapter.d.ts +1 -1
- package/dist/adapters/langchain-adapter.d.mts +1 -1
- package/dist/adapters/langchain-adapter.d.ts +1 -1
- package/dist/adapters/mastra-adapter.d.mts +1 -1
- package/dist/adapters/mastra-adapter.d.ts +1 -1
- package/dist/client/index.d.mts +1 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +14 -5
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +14 -5
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.js +15 -6
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +15 -6
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.js +15 -6
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +15 -6
- package/dist/client/vue.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +201 -158
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +201 -158
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-FAFpUzZ4.d.ts → multi-session-client-BYLarghq.d.ts} +29 -19
- package/dist/{multi-session-client-DzjmT7FX.d.mts → multi-session-client-CzhMkE0k.d.mts} +29 -19
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +193 -151
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +193 -151
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +2 -2
- package/dist/shared/index.d.ts +2 -2
- package/dist/shared/index.js +2 -2
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +2 -2
- package/dist/shared/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client/core/sse-client.ts +371 -354
- package/src/client/react/use-mcp.ts +31 -31
- package/src/client/vue/use-mcp.ts +77 -77
- package/src/server/handlers/nextjs-handler.ts +194 -197
- package/src/server/handlers/sse-handler.ts +62 -111
- package/src/server/mcp/oauth-client.ts +67 -79
- package/src/server/mcp/storage-oauth-provider.ts +71 -38
- package/src/server/storage/index.ts +15 -13
- package/src/server/storage/redis-backend.ts +93 -23
- package/src/server/storage/types.ts +12 -12
- package/src/shared/constants.ts +2 -2
- package/src/shared/event-routing.ts +28 -0
|
@@ -1,207 +1,204 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Next.js App Router Handler for MCP
|
|
3
|
-
* Stateless transport for serverless environments:
|
|
4
|
-
* - POST + `Accept: text/event-stream` streams progress + rpc-response
|
|
5
|
-
* - POST + JSON accepts direct RPC result response
|
|
6
|
-
*/
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Next.js App Router Handler for MCP
|
|
3
|
+
* Stateless transport for serverless environments:
|
|
4
|
+
* - POST + `Accept: text/event-stream` streams progress + rpc-response
|
|
5
|
+
* - POST + JSON accepts direct RPC result response
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
8
|
import { SSEConnectionManager, type ClientMetadata } from './sse-handler.js';
|
|
9
9
|
import type { McpConnectionEvent, McpObservabilityEvent } from '../../shared/events.js';
|
|
10
|
+
import { isConnectionEvent, isRpcResponseEvent } from '../../shared/event-routing.js';
|
|
10
11
|
import type { McpRpcResponse } from '../../shared/types.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
()
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const manager = new SSEConnectionManager(
|
|
144
|
-
toManagerOptions(identity, resolvedClientMetadata),
|
|
12
|
+
|
|
13
|
+
export interface NextMcpHandlerOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Extract identity from request (default: from 'identity' query param)
|
|
16
|
+
*/
|
|
17
|
+
getIdentity?: (request: Request) => string | null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract auth token from request (default: from 'token' query param or Authorization header)
|
|
21
|
+
*/
|
|
22
|
+
getAuthToken?: (request: Request) => string | null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Authenticate user and verify access (optional)
|
|
26
|
+
* Return true if user is authenticated, false otherwise
|
|
27
|
+
*/
|
|
28
|
+
authenticate?: (identity: string, token: string | null) => Promise<boolean> | boolean;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Heartbeat interval in milliseconds (default: 30000)
|
|
32
|
+
*/
|
|
33
|
+
heartbeatInterval?: number;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Static OAuth client metadata defaults (for all connections)
|
|
37
|
+
*/
|
|
38
|
+
clientDefaults?: ClientMetadata;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Dynamic OAuth client metadata getter (per-request)
|
|
42
|
+
*/
|
|
43
|
+
getClientMetadata?: (request: Request) => ClientMetadata | Promise<ClientMetadata>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createNextMcpHandler(options: NextMcpHandlerOptions = {}) {
|
|
47
|
+
const {
|
|
48
|
+
getIdentity = (request: Request) => new URL(request.url).searchParams.get('identity'),
|
|
49
|
+
getAuthToken = (request: Request) => {
|
|
50
|
+
const url = new URL(request.url);
|
|
51
|
+
return url.searchParams.get('token') || request.headers.get('authorization');
|
|
52
|
+
},
|
|
53
|
+
authenticate = () => true,
|
|
54
|
+
heartbeatInterval = 30000,
|
|
55
|
+
clientDefaults,
|
|
56
|
+
getClientMetadata,
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
const toManagerOptions = (identity: string, resolvedClientMetadata?: ClientMetadata) => ({
|
|
60
|
+
identity,
|
|
61
|
+
heartbeatInterval,
|
|
62
|
+
clientDefaults: resolvedClientMetadata,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
async function resolveClientMetadata(request: Request): Promise<ClientMetadata | undefined> {
|
|
66
|
+
return getClientMetadata ? await getClientMetadata(request) : clientDefaults;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function GET(): Promise<Response> {
|
|
70
|
+
return Response.json(
|
|
71
|
+
{
|
|
72
|
+
error: {
|
|
73
|
+
code: 'METHOD_NOT_ALLOWED',
|
|
74
|
+
message: 'Use POST /api/mcp. For streaming use Accept: text/event-stream.',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{ status: 405 }
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function POST(request: Request): Promise<Response> {
|
|
82
|
+
const identity = getIdentity(request);
|
|
83
|
+
const authToken = getAuthToken(request);
|
|
84
|
+
const acceptsEventStream = (request.headers.get('accept') || '').toLowerCase().includes('text/event-stream');
|
|
85
|
+
|
|
86
|
+
if (!identity) {
|
|
87
|
+
return Response.json({ error: { code: 'MISSING_IDENTITY', message: 'Missing identity' } }, { status: 400 });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const isAuthorized = await authenticate(identity, authToken);
|
|
91
|
+
if (!isAuthorized) {
|
|
92
|
+
return Response.json({ error: { code: 'UNAUTHORIZED', message: 'Unauthorized' } }, { status: 401 });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let rawBody = '';
|
|
96
|
+
try {
|
|
97
|
+
rawBody = await request.text();
|
|
98
|
+
const body = rawBody ? JSON.parse(rawBody) : null;
|
|
99
|
+
|
|
100
|
+
if (!body || typeof body !== 'object') {
|
|
101
|
+
return Response.json(
|
|
102
|
+
{
|
|
103
|
+
error: {
|
|
104
|
+
code: 'INVALID_REQUEST',
|
|
105
|
+
message: 'Invalid JSON-RPC request body',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{ status: 400 }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const resolvedClientMetadata = await resolveClientMetadata(request);
|
|
113
|
+
|
|
114
|
+
if (!acceptsEventStream) {
|
|
115
|
+
const manager = new SSEConnectionManager(
|
|
116
|
+
toManagerOptions(identity, resolvedClientMetadata),
|
|
117
|
+
() => { }
|
|
118
|
+
);
|
|
119
|
+
try {
|
|
120
|
+
const response = await manager.handleRequest(body as any);
|
|
121
|
+
return Response.json(response);
|
|
122
|
+
} finally {
|
|
123
|
+
manager.dispose();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const stream = new TransformStream();
|
|
128
|
+
const writer = stream.writable.getWriter();
|
|
129
|
+
const encoder = new TextEncoder();
|
|
130
|
+
let streamWritable = true;
|
|
131
|
+
|
|
132
|
+
const sendSSE = (event: string, data: unknown) => {
|
|
133
|
+
if (!streamWritable) return;
|
|
134
|
+
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
135
|
+
writer.write(encoder.encode(message)).catch(() => {
|
|
136
|
+
streamWritable = false;
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const manager = new SSEConnectionManager(
|
|
141
|
+
toManagerOptions(identity, resolvedClientMetadata),
|
|
145
142
|
(event: McpConnectionEvent | McpObservabilityEvent | McpRpcResponse) => {
|
|
146
143
|
if (isRpcResponseEvent(event)) {
|
|
147
144
|
sendSSE('rpc-response', event);
|
|
148
|
-
} else if (
|
|
145
|
+
} else if (isConnectionEvent(event)) {
|
|
149
146
|
sendSSE('connection', event);
|
|
150
147
|
} else {
|
|
151
148
|
sendSSE('observability', event);
|
|
152
149
|
}
|
|
153
|
-
}
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
sendSSE('connected', { timestamp: Date.now() });
|
|
157
|
-
|
|
158
|
-
void (async () => {
|
|
159
|
-
try {
|
|
160
|
-
await manager.handleRequest(body as any);
|
|
161
|
-
} catch (error) {
|
|
162
|
-
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
163
|
-
sendSSE('rpc-response', {
|
|
164
|
-
id: (body as any).id || 'unknown',
|
|
165
|
-
error: {
|
|
166
|
-
code: 'EXECUTION_ERROR',
|
|
167
|
-
message: err.message,
|
|
168
|
-
},
|
|
169
|
-
} satisfies McpRpcResponse);
|
|
170
|
-
} finally {
|
|
171
|
-
streamWritable = false;
|
|
172
|
-
manager.dispose();
|
|
173
|
-
writer.close().catch(() => { });
|
|
174
|
-
}
|
|
175
|
-
})();
|
|
176
|
-
|
|
177
|
-
return new Response(stream.readable, {
|
|
178
|
-
status: 200,
|
|
179
|
-
headers: {
|
|
180
|
-
'Content-Type': 'text/event-stream',
|
|
181
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
182
|
-
'Connection': 'keep-alive',
|
|
183
|
-
'X-Accel-Buffering': 'no',
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
} catch (error) {
|
|
187
|
-
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
188
|
-
console.error('[MCP Next Handler] Failed to handle RPC', {
|
|
189
|
-
identity,
|
|
190
|
-
message: err.message,
|
|
191
|
-
stack: err.stack,
|
|
192
|
-
rawBody: rawBody.slice(0, 500),
|
|
193
|
-
});
|
|
194
|
-
return Response.json(
|
|
195
|
-
{
|
|
196
|
-
error: {
|
|
197
|
-
code: 'EXECUTION_ERROR',
|
|
198
|
-
message: err.message,
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
{ status: 500 }
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return { GET, POST };
|
|
207
|
-
}
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
sendSSE('connected', { timestamp: Date.now() });
|
|
154
|
+
|
|
155
|
+
void (async () => {
|
|
156
|
+
try {
|
|
157
|
+
await manager.handleRequest(body as any);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
160
|
+
sendSSE('rpc-response', {
|
|
161
|
+
id: (body as any).id || 'unknown',
|
|
162
|
+
error: {
|
|
163
|
+
code: 'EXECUTION_ERROR',
|
|
164
|
+
message: err.message,
|
|
165
|
+
},
|
|
166
|
+
} satisfies McpRpcResponse);
|
|
167
|
+
} finally {
|
|
168
|
+
streamWritable = false;
|
|
169
|
+
manager.dispose();
|
|
170
|
+
writer.close().catch(() => { });
|
|
171
|
+
}
|
|
172
|
+
})();
|
|
173
|
+
|
|
174
|
+
return new Response(stream.readable, {
|
|
175
|
+
status: 200,
|
|
176
|
+
headers: {
|
|
177
|
+
'Content-Type': 'text/event-stream',
|
|
178
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
179
|
+
'Connection': 'keep-alive',
|
|
180
|
+
'X-Accel-Buffering': 'no',
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
185
|
+
console.error('[MCP Next Handler] Failed to handle RPC', {
|
|
186
|
+
identity,
|
|
187
|
+
message: err.message,
|
|
188
|
+
stack: err.stack,
|
|
189
|
+
rawBody: rawBody.slice(0, 500),
|
|
190
|
+
});
|
|
191
|
+
return Response.json(
|
|
192
|
+
{
|
|
193
|
+
error: {
|
|
194
|
+
code: 'EXECUTION_ERROR',
|
|
195
|
+
message: err.message,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{ status: 500 }
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { GET, POST };
|
|
204
|
+
}
|
|
@@ -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,10 +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 {
|
|
38
|
-
import {
|
|
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';
|
|
39
41
|
|
|
40
42
|
// ============================================
|
|
41
43
|
// Types & Interfaces
|
|
@@ -79,7 +81,7 @@ const DEFAULT_HEARTBEAT_INTERVAL = 30000;
|
|
|
79
81
|
* Manages a single SSE connection and handles MCP operations.
|
|
80
82
|
* Each instance corresponds to one connected browser client.
|
|
81
83
|
*/
|
|
82
|
-
export class SSEConnectionManager {
|
|
84
|
+
export class SSEConnectionManager {
|
|
83
85
|
private readonly identity: string;
|
|
84
86
|
private readonly clients = new Map<string, MCPClient>();
|
|
85
87
|
private heartbeatTimer?: NodeJS.Timeout;
|
|
@@ -234,7 +236,7 @@ export class SSEConnectionManager {
|
|
|
234
236
|
/**
|
|
235
237
|
* Connect to an MCP server
|
|
236
238
|
*/
|
|
237
|
-
private async connect(params: ConnectParams): Promise<ConnectResult> {
|
|
239
|
+
private async connect(params: ConnectParams): Promise<ConnectResult> {
|
|
238
240
|
const { serverName, serverUrl, callbackUrl, transportType } = params;
|
|
239
241
|
|
|
240
242
|
// Normalize serverId to max 12 chars to keep tool names under 64 chars (DeepSeek/OpenAI limits)
|
|
@@ -265,43 +267,21 @@ export class SSEConnectionManager {
|
|
|
265
267
|
// Generate session ID
|
|
266
268
|
const sessionId = await storage.generateSessionId();
|
|
267
269
|
|
|
268
|
-
|
|
269
|
-
this.emitConnectionEvent({
|
|
270
|
-
type: 'state_changed',
|
|
271
|
-
sessionId,
|
|
272
|
-
serverId,
|
|
273
|
-
serverName,
|
|
274
|
-
serverUrl,
|
|
275
|
-
state: 'CONNECTING',
|
|
276
|
-
previousState: 'DISCONNECTED',
|
|
277
|
-
timestamp: Date.now(),
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
try {
|
|
270
|
+
try {
|
|
281
271
|
// Get resolved client metadata
|
|
282
272
|
const clientMetadata = await this.getResolvedClientMetadata();
|
|
283
273
|
|
|
284
274
|
// Create MCP client
|
|
285
|
-
const client = new MCPClient({
|
|
286
|
-
identity: this.identity,
|
|
287
|
-
sessionId,
|
|
288
|
-
serverId,
|
|
289
|
-
serverName,
|
|
290
|
-
serverUrl,
|
|
291
|
-
callbackUrl,
|
|
292
|
-
transportType,
|
|
293
|
-
...clientMetadata, // Spread client metadata (clientName, clientUri, logoUri, policyUri)
|
|
294
|
-
|
|
295
|
-
// Emit auth required event
|
|
296
|
-
this.emitConnectionEvent({
|
|
297
|
-
type: 'auth_required',
|
|
298
|
-
sessionId,
|
|
299
|
-
serverId,
|
|
300
|
-
authUrl,
|
|
301
|
-
timestamp: Date.now(),
|
|
302
|
-
});
|
|
303
|
-
},
|
|
304
|
-
});
|
|
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
|
+
});
|
|
305
285
|
|
|
306
286
|
// Note: Session will be created by MCPClient after successful connection
|
|
307
287
|
// This ensures sessions only exist for successful or OAuth-pending connections
|
|
@@ -322,25 +302,25 @@ export class SSEConnectionManager {
|
|
|
322
302
|
await client.connect();
|
|
323
303
|
|
|
324
304
|
// Fetch tools
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
this.emitConnectionEvent({
|
|
328
|
-
type: 'tools_discovered',
|
|
329
|
-
sessionId,
|
|
330
|
-
serverId,
|
|
331
|
-
toolCount: tools.tools.length,
|
|
332
|
-
tools: tools.tools,
|
|
333
|
-
timestamp: Date.now(),
|
|
334
|
-
});
|
|
305
|
+
await client.listTools();
|
|
335
306
|
|
|
336
307
|
return {
|
|
337
308
|
sessionId,
|
|
338
309
|
success: true,
|
|
339
310
|
};
|
|
340
|
-
} catch (error) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
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,
|
|
344
324
|
serverId,
|
|
345
325
|
error: error instanceof Error ? error.message : 'Connection failed',
|
|
346
326
|
errorType: 'connection',
|
|
@@ -468,15 +448,6 @@ export class SSEConnectionManager {
|
|
|
468
448
|
|
|
469
449
|
const tools = await client.listTools();
|
|
470
450
|
|
|
471
|
-
this.emitConnectionEvent({
|
|
472
|
-
type: 'tools_discovered',
|
|
473
|
-
sessionId,
|
|
474
|
-
serverId: session.serverId ?? 'unknown',
|
|
475
|
-
toolCount: tools.tools.length,
|
|
476
|
-
tools: tools.tools,
|
|
477
|
-
timestamp: Date.now(),
|
|
478
|
-
});
|
|
479
|
-
|
|
480
451
|
return { success: true, toolCount: tools.tools.length };
|
|
481
452
|
} catch (error) {
|
|
482
453
|
this.emitConnectionEvent({
|
|
@@ -495,29 +466,18 @@ export class SSEConnectionManager {
|
|
|
495
466
|
/**
|
|
496
467
|
* Complete OAuth authorization flow
|
|
497
468
|
*/
|
|
498
|
-
private async finishAuth(params: FinishAuthParams): Promise<FinishAuthResult> {
|
|
499
|
-
const { sessionId, code } = params;
|
|
500
|
-
|
|
501
|
-
const session = await storage.getSession(this.identity, sessionId);
|
|
502
|
-
if (!session) {
|
|
503
|
-
throw new Error('Session not found');
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
serverName: session.serverName ?? 'Unknown',
|
|
511
|
-
serverUrl: session.serverUrl,
|
|
512
|
-
state: 'AUTHENTICATING',
|
|
513
|
-
previousState: 'DISCONNECTED',
|
|
514
|
-
timestamp: Date.now(),
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
try {
|
|
518
|
-
const client = new MCPClient({
|
|
519
|
-
identity: this.identity,
|
|
520
|
-
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,
|
|
521
481
|
});
|
|
522
482
|
|
|
523
483
|
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
@@ -527,15 +487,6 @@ export class SSEConnectionManager {
|
|
|
527
487
|
|
|
528
488
|
const tools = await client.listTools();
|
|
529
489
|
|
|
530
|
-
this.emitConnectionEvent({
|
|
531
|
-
type: 'tools_discovered',
|
|
532
|
-
sessionId,
|
|
533
|
-
serverId: session.serverId ?? 'unknown',
|
|
534
|
-
toolCount: tools.tools.length,
|
|
535
|
-
tools: tools.tools,
|
|
536
|
-
timestamp: Date.now(),
|
|
537
|
-
});
|
|
538
|
-
|
|
539
490
|
return { success: true, toolCount: tools.tools.length };
|
|
540
491
|
} catch (error) {
|
|
541
492
|
this.emitConnectionEvent({
|
|
@@ -622,8 +573,8 @@ export class SSEConnectionManager {
|
|
|
622
573
|
* Create an SSE endpoint handler compatible with Node.js HTTP frameworks.
|
|
623
574
|
* Handles both SSE streaming (GET) and RPC requests (POST).
|
|
624
575
|
*/
|
|
625
|
-
export function createSSEHandler(options: SSEHandlerOptions) {
|
|
626
|
-
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 }) => {
|
|
627
578
|
// Set SSE headers
|
|
628
579
|
res.writeHead(200, {
|
|
629
580
|
'Content-Type': 'text/event-stream',
|
|
@@ -635,15 +586,15 @@ export function createSSEHandler(options: SSEHandlerOptions) {
|
|
|
635
586
|
// Send initial connection acknowledgment
|
|
636
587
|
writeSSEEvent(res, 'connected', { timestamp: Date.now() });
|
|
637
588
|
|
|
638
|
-
// Create connection manager with event routing
|
|
639
|
-
const manager = new SSEConnectionManager(options, (event) => {
|
|
640
|
-
if (
|
|
641
|
-
writeSSEEvent(res, 'rpc-response', event);
|
|
642
|
-
} else if (
|
|
643
|
-
writeSSEEvent(res, 'connection', event);
|
|
644
|
-
} else {
|
|
645
|
-
writeSSEEvent(res, 'observability', event);
|
|
646
|
-
}
|
|
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
|
+
}
|
|
647
598
|
});
|
|
648
599
|
|
|
649
600
|
// Cleanup on client disconnect
|
|
@@ -674,7 +625,7 @@ export function createSSEHandler(options: SSEHandlerOptions) {
|
|
|
674
625
|
/**
|
|
675
626
|
* Write an SSE event to the response stream
|
|
676
627
|
*/
|
|
677
|
-
function writeSSEEvent(res: { write: Function }, event: string, data: unknown): void {
|
|
678
|
-
res.write(`event: ${event}\n`);
|
|
679
|
-
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
680
|
-
}
|
|
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
|
+
}
|