@decocms/runtime 1.0.0-alpha.22 → 1.0.0-alpha.24
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/package.json +2 -4
- package/src/index.ts +60 -1
- package/src/oauth.ts +490 -0
- package/src/proxy.ts +1 -157
- package/src/tools.ts +65 -0
- package/src/http-client-transport.ts +0 -1
- package/src/mcp-client.ts +0 -139
package/package.json
CHANGED
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/runtime",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@cloudflare/workers-types": "^4.20250617.0",
|
|
7
7
|
"@deco/mcp": "npm:@jsr/deco__mcp@0.5.5",
|
|
8
|
-
"@decocms/bindings": "1.0.1-alpha.
|
|
8
|
+
"@decocms/bindings": "1.0.1-alpha.14",
|
|
9
9
|
"@modelcontextprotocol/sdk": "1.20.2",
|
|
10
10
|
"@ai-sdk/provider": "^2.0.0",
|
|
11
11
|
"hono": "^4.10.7",
|
|
12
12
|
"jose": "^6.0.11",
|
|
13
13
|
"zod": "^3.25.76",
|
|
14
|
-
"zod-from-json-schema": "^0.0.5",
|
|
15
14
|
"zod-to-json-schema": "3.25.0"
|
|
16
15
|
},
|
|
17
16
|
"exports": {
|
|
18
17
|
".": "./src/index.ts",
|
|
19
18
|
"./proxy": "./src/proxy.ts",
|
|
20
19
|
"./client": "./src/client.ts",
|
|
21
|
-
"./mcp-client": "./src/mcp-client.ts",
|
|
22
20
|
"./bindings": "./src/bindings/index.ts",
|
|
23
21
|
"./asset-server": "./src/asset-server/index.ts",
|
|
24
22
|
"./tools": "./src/tools.ts"
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { decodeJwt } from "jose";
|
|
|
3
3
|
import type { z } from "zod";
|
|
4
4
|
import { createContractBinding, createIntegrationBinding } from "./bindings.ts";
|
|
5
5
|
import { type CORSOptions, handlePreflight, withCORS } from "./cors.ts";
|
|
6
|
+
import { createOAuthHandlers } from "./oauth.ts";
|
|
6
7
|
import { State } from "./state.ts";
|
|
7
8
|
import {
|
|
8
9
|
createMCPServer,
|
|
@@ -82,6 +83,7 @@ export interface RequestContext<TSchema extends z.ZodTypeAny = any> {
|
|
|
82
83
|
state: z.infer<TSchema>;
|
|
83
84
|
token: string;
|
|
84
85
|
meshUrl: string;
|
|
86
|
+
authorization?: string | null;
|
|
85
87
|
ensureAuthenticated: (options?: {
|
|
86
88
|
workspaceHint?: string;
|
|
87
89
|
}) => User | undefined;
|
|
@@ -158,14 +160,19 @@ export const withBindings = <TEnv>({
|
|
|
158
160
|
tokenOrContext,
|
|
159
161
|
url,
|
|
160
162
|
bindings: inlineBindings,
|
|
163
|
+
authToken,
|
|
161
164
|
}: {
|
|
162
165
|
env: TEnv;
|
|
163
166
|
server: MCPServer<TEnv, any>;
|
|
167
|
+
// token is x-mesh-token
|
|
164
168
|
tokenOrContext?: string | RequestContext;
|
|
169
|
+
// authToken is the authorization header
|
|
170
|
+
authToken?: string | null;
|
|
165
171
|
url?: string;
|
|
166
172
|
bindings?: Binding[];
|
|
167
173
|
}): TEnv => {
|
|
168
174
|
const env = _env as DefaultEnv<any>;
|
|
175
|
+
const authorization = authToken ? authToken.split(" ")[1] : undefined;
|
|
169
176
|
|
|
170
177
|
let context;
|
|
171
178
|
if (typeof tokenOrContext === "string") {
|
|
@@ -179,6 +186,7 @@ export const withBindings = <TEnv>({
|
|
|
179
186
|
}) ?? {};
|
|
180
187
|
|
|
181
188
|
context = {
|
|
189
|
+
authorization,
|
|
182
190
|
state: decoded.state ?? metadata.state,
|
|
183
191
|
token: tokenOrContext,
|
|
184
192
|
meshUrl: (decoded.meshUrl as string) ?? metadata.meshUrl,
|
|
@@ -196,6 +204,7 @@ export const withBindings = <TEnv>({
|
|
|
196
204
|
connectionId?: string;
|
|
197
205
|
}) ?? {};
|
|
198
206
|
const appName = decoded.appName as string | undefined;
|
|
207
|
+
context.authorization ??= authorization;
|
|
199
208
|
context.callerApp = appName;
|
|
200
209
|
context.connectionId ??=
|
|
201
210
|
(decoded.connectionId as string) ?? metadata.connectionId;
|
|
@@ -203,6 +212,7 @@ export const withBindings = <TEnv>({
|
|
|
203
212
|
} else {
|
|
204
213
|
context = {
|
|
205
214
|
state: {},
|
|
215
|
+
authorization,
|
|
206
216
|
token: undefined,
|
|
207
217
|
meshUrl: undefined,
|
|
208
218
|
connectionId: undefined,
|
|
@@ -233,6 +243,8 @@ export const withRuntime = <TEnv, TSchema extends z.ZodTypeAny = never>(
|
|
|
233
243
|
) => {
|
|
234
244
|
const server = createMCPServer<TEnv, TSchema>(userFns);
|
|
235
245
|
const corsOptions = userFns.cors;
|
|
246
|
+
const oauth = userFns.oauth;
|
|
247
|
+
const oauthHandlers = oauth ? createOAuthHandlers(oauth) : null;
|
|
236
248
|
|
|
237
249
|
const fetcher = async (
|
|
238
250
|
req: Request,
|
|
@@ -240,7 +252,53 @@ export const withRuntime = <TEnv, TSchema extends z.ZodTypeAny = never>(
|
|
|
240
252
|
ctx: any,
|
|
241
253
|
) => {
|
|
242
254
|
const url = new URL(req.url);
|
|
255
|
+
|
|
256
|
+
// OAuth routes (when configured)
|
|
257
|
+
if (oauthHandlers) {
|
|
258
|
+
// Protected resource metadata (RFC9728) - both paths MUST be supported
|
|
259
|
+
if (
|
|
260
|
+
url.pathname === "/.well-known/oauth-protected-resource" ||
|
|
261
|
+
url.pathname === "/mcp/.well-known/oauth-protected-resource"
|
|
262
|
+
) {
|
|
263
|
+
return oauthHandlers.handleProtectedResourceMetadata(req);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Authorization server metadata (RFC8414)
|
|
267
|
+
if (url.pathname === "/.well-known/oauth-authorization-server") {
|
|
268
|
+
return oauthHandlers.handleAuthorizationServerMetadata(req);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Authorization endpoint - redirects to external OAuth provider
|
|
272
|
+
if (url.pathname === "/authorize") {
|
|
273
|
+
return oauthHandlers.handleAuthorize(req);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// OAuth callback - receives code from external OAuth provider
|
|
277
|
+
if (url.pathname === "/oauth/callback") {
|
|
278
|
+
return oauthHandlers.handleOAuthCallback(req);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Token endpoint - exchanges code for tokens
|
|
282
|
+
if (url.pathname === "/token" && req.method === "POST") {
|
|
283
|
+
return oauthHandlers.handleToken(req);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Dynamic client registration (RFC7591)
|
|
287
|
+
if (
|
|
288
|
+
(url.pathname === "/register" || url.pathname === "/mcp/register") &&
|
|
289
|
+
req.method === "POST"
|
|
290
|
+
) {
|
|
291
|
+
return oauthHandlers.handleClientRegistration(req);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// MCP endpoint
|
|
243
296
|
if (url.pathname === "/mcp") {
|
|
297
|
+
// If OAuth is configured, require authentication
|
|
298
|
+
if (oauthHandlers && !oauthHandlers.hasAuth(req)) {
|
|
299
|
+
return oauthHandlers.createUnauthorizedResponse(req);
|
|
300
|
+
}
|
|
301
|
+
|
|
244
302
|
return server.fetch(req, env, ctx);
|
|
245
303
|
}
|
|
246
304
|
|
|
@@ -273,7 +331,7 @@ export const withRuntime = <TEnv, TSchema extends z.ZodTypeAny = never>(
|
|
|
273
331
|
};
|
|
274
332
|
|
|
275
333
|
return {
|
|
276
|
-
fetch: async (req: Request, env: TEnv & DefaultEnv<TSchema>, ctx
|
|
334
|
+
fetch: async (req: Request, env: TEnv & DefaultEnv<TSchema>, ctx?: any) => {
|
|
277
335
|
// Handle CORS preflight (OPTIONS) requests
|
|
278
336
|
if (corsOptions !== false && req.method === "OPTIONS") {
|
|
279
337
|
const options = corsOptions ?? {};
|
|
@@ -281,6 +339,7 @@ export const withRuntime = <TEnv, TSchema extends z.ZodTypeAny = never>(
|
|
|
281
339
|
}
|
|
282
340
|
|
|
283
341
|
const bindings = withBindings({
|
|
342
|
+
authToken: req.headers.get("authorization") ?? null,
|
|
284
343
|
env,
|
|
285
344
|
server,
|
|
286
345
|
bindings: userFns.bindings,
|
package/src/oauth.ts
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import type { OAuthClient, OAuthConfig, OAuthParams } from "./tools.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a cryptographically secure random token
|
|
5
|
+
*/
|
|
6
|
+
function generateRandomToken(length = 32): string {
|
|
7
|
+
const chars =
|
|
8
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
9
|
+
const array = new Uint8Array(length);
|
|
10
|
+
crypto.getRandomValues(array);
|
|
11
|
+
return Array.from(array, (byte) => chars[byte % chars.length]).join("");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate redirect URI format per OAuth 2.1
|
|
16
|
+
*/
|
|
17
|
+
function isValidRedirectUri(uri: string): boolean {
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(uri);
|
|
20
|
+
return (
|
|
21
|
+
url.protocol === "https:" ||
|
|
22
|
+
url.hostname === "localhost" ||
|
|
23
|
+
url.hostname === "127.0.0.1" ||
|
|
24
|
+
// Allow custom schemes for native apps (e.g., cursor://, vscode://)
|
|
25
|
+
!url.protocol.startsWith("http")
|
|
26
|
+
);
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Encode data as base64url JSON
|
|
34
|
+
*/
|
|
35
|
+
function encodeState<T>(data: T): string {
|
|
36
|
+
return btoa(JSON.stringify(data))
|
|
37
|
+
.replace(/\+/g, "-")
|
|
38
|
+
.replace(/\//g, "_")
|
|
39
|
+
.replace(/=+$/, "");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Decode base64url JSON data
|
|
44
|
+
*/
|
|
45
|
+
function decodeState<T>(encoded: string): T | null {
|
|
46
|
+
try {
|
|
47
|
+
const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
|
|
48
|
+
return JSON.parse(atob(base64)) as T;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface PendingAuthState {
|
|
55
|
+
redirectUri: string;
|
|
56
|
+
clientState?: string;
|
|
57
|
+
codeChallenge?: string;
|
|
58
|
+
codeChallengeMethod?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface CodePayload {
|
|
62
|
+
accessToken: string;
|
|
63
|
+
tokenType: string;
|
|
64
|
+
codeChallenge?: string;
|
|
65
|
+
codeChallengeMethod?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create OAuth endpoint handlers for MCP servers
|
|
70
|
+
* The MCP server acts as an OAuth Authorization Server proxy
|
|
71
|
+
* Stateless implementation - no persistence required
|
|
72
|
+
* Per MCP Authorization spec: https://modelcontextprotocol.io/specification/draft/basic/authorization
|
|
73
|
+
*/
|
|
74
|
+
export function createOAuthHandlers(oauth: OAuthConfig) {
|
|
75
|
+
/**
|
|
76
|
+
* Build OAuth 2.0 Protected Resource Metadata (RFC9728)
|
|
77
|
+
* Points to THIS server as the authorization server
|
|
78
|
+
*/
|
|
79
|
+
const handleProtectedResourceMetadata = (req: Request): Response => {
|
|
80
|
+
const url = new URL(req.url);
|
|
81
|
+
const resourceUrl = `${url.origin}/mcp`;
|
|
82
|
+
|
|
83
|
+
return Response.json({
|
|
84
|
+
resource: resourceUrl,
|
|
85
|
+
// Point to ourselves - we are the authorization server proxy
|
|
86
|
+
authorization_servers: [url.origin],
|
|
87
|
+
scopes_supported: ["*"],
|
|
88
|
+
bearer_methods_supported: ["header"],
|
|
89
|
+
resource_signing_alg_values_supported: ["RS256", "none"],
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build OAuth 2.0 Authorization Server Metadata (RFC8414)
|
|
95
|
+
* Exposes our endpoints for authorization, token exchange, and registration
|
|
96
|
+
*/
|
|
97
|
+
const handleAuthorizationServerMetadata = (req: Request): Response => {
|
|
98
|
+
const url = new URL(req.url);
|
|
99
|
+
const baseUrl = url.origin;
|
|
100
|
+
|
|
101
|
+
return Response.json({
|
|
102
|
+
issuer: baseUrl,
|
|
103
|
+
authorization_endpoint: `${baseUrl}/authorize`,
|
|
104
|
+
token_endpoint: `${baseUrl}/token`,
|
|
105
|
+
registration_endpoint: `${baseUrl}/register`,
|
|
106
|
+
scopes_supported: ["*"],
|
|
107
|
+
response_types_supported: ["code"],
|
|
108
|
+
response_modes_supported: ["query"],
|
|
109
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
110
|
+
token_endpoint_auth_methods_supported: ["none", "client_secret_post"],
|
|
111
|
+
code_challenge_methods_supported: ["S256", "plain"],
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Handle authorization request - redirects to external OAuth provider
|
|
117
|
+
* Stateless: encodes all needed info in the state parameter
|
|
118
|
+
*/
|
|
119
|
+
const handleAuthorize = (req: Request): Response => {
|
|
120
|
+
const url = new URL(req.url);
|
|
121
|
+
const redirectUri = url.searchParams.get("redirect_uri");
|
|
122
|
+
const responseType = url.searchParams.get("response_type");
|
|
123
|
+
const clientState = url.searchParams.get("state");
|
|
124
|
+
const codeChallenge = url.searchParams.get("code_challenge");
|
|
125
|
+
const codeChallengeMethod = url.searchParams.get("code_challenge_method");
|
|
126
|
+
|
|
127
|
+
// Validate required params
|
|
128
|
+
if (!redirectUri) {
|
|
129
|
+
return Response.json(
|
|
130
|
+
{
|
|
131
|
+
error: "invalid_request",
|
|
132
|
+
error_description: "redirect_uri required",
|
|
133
|
+
},
|
|
134
|
+
{ status: 400 },
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (responseType !== "code") {
|
|
139
|
+
return Response.json(
|
|
140
|
+
{
|
|
141
|
+
error: "unsupported_response_type",
|
|
142
|
+
error_description: "Only 'code' is supported",
|
|
143
|
+
},
|
|
144
|
+
{ status: 400 },
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Encode pending auth state
|
|
149
|
+
const pendingState: PendingAuthState = {
|
|
150
|
+
redirectUri,
|
|
151
|
+
clientState: clientState ?? undefined,
|
|
152
|
+
codeChallenge: codeChallenge ?? undefined,
|
|
153
|
+
codeChallengeMethod: codeChallengeMethod ?? undefined,
|
|
154
|
+
};
|
|
155
|
+
const encodedState = encodeState(pendingState);
|
|
156
|
+
|
|
157
|
+
// Build callback URL pointing to our internal callback
|
|
158
|
+
const callbackUrl = new URL(`${url.origin}/oauth/callback`);
|
|
159
|
+
callbackUrl.searchParams.set("state", encodedState);
|
|
160
|
+
|
|
161
|
+
// Get the external authorization URL from the config
|
|
162
|
+
const externalAuthUrl = oauth.authorizationUrl(callbackUrl.toString());
|
|
163
|
+
|
|
164
|
+
// Redirect to external OAuth provider
|
|
165
|
+
return Response.redirect(externalAuthUrl, 302);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Handle OAuth callback from external provider
|
|
170
|
+
* Stateless: decodes state to get redirect info, encodes token in code
|
|
171
|
+
*/
|
|
172
|
+
const handleOAuthCallback = async (req: Request): Promise<Response> => {
|
|
173
|
+
const url = new URL(req.url);
|
|
174
|
+
const code = url.searchParams.get("code");
|
|
175
|
+
const encodedState = url.searchParams.get("state");
|
|
176
|
+
const error = url.searchParams.get("error");
|
|
177
|
+
|
|
178
|
+
// Decode state
|
|
179
|
+
const pending = encodedState
|
|
180
|
+
? decodeState<PendingAuthState>(encodedState)
|
|
181
|
+
: null;
|
|
182
|
+
|
|
183
|
+
if (error) {
|
|
184
|
+
const errorDescription =
|
|
185
|
+
url.searchParams.get("error_description") ?? "Authorization failed";
|
|
186
|
+
if (pending?.redirectUri) {
|
|
187
|
+
const redirectUrl = new URL(pending.redirectUri);
|
|
188
|
+
redirectUrl.searchParams.set("error", error);
|
|
189
|
+
redirectUrl.searchParams.set("error_description", errorDescription);
|
|
190
|
+
if (pending.clientState)
|
|
191
|
+
redirectUrl.searchParams.set("state", pending.clientState);
|
|
192
|
+
return Response.redirect(redirectUrl.toString(), 302);
|
|
193
|
+
}
|
|
194
|
+
return Response.json(
|
|
195
|
+
{ error, error_description: errorDescription },
|
|
196
|
+
{ status: 400 },
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!code || !pending) {
|
|
201
|
+
return Response.json(
|
|
202
|
+
{
|
|
203
|
+
error: "invalid_request",
|
|
204
|
+
error_description: "Missing code or state",
|
|
205
|
+
},
|
|
206
|
+
{ status: 400 },
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
// Exchange code with external provider
|
|
212
|
+
const oauthParams: OAuthParams = { code };
|
|
213
|
+
const tokenResponse = await oauth.exchangeCode(oauthParams);
|
|
214
|
+
|
|
215
|
+
// Encode the token in our own code (stateless)
|
|
216
|
+
const codePayload: CodePayload = {
|
|
217
|
+
accessToken: tokenResponse.access_token,
|
|
218
|
+
tokenType: tokenResponse.token_type,
|
|
219
|
+
codeChallenge: pending.codeChallenge,
|
|
220
|
+
codeChallengeMethod: pending.codeChallengeMethod,
|
|
221
|
+
};
|
|
222
|
+
const ourCode = encodeState(codePayload);
|
|
223
|
+
|
|
224
|
+
// Redirect back to client with our code
|
|
225
|
+
const redirectUrl = new URL(pending.redirectUri);
|
|
226
|
+
redirectUrl.searchParams.set("code", ourCode);
|
|
227
|
+
if (pending.clientState) {
|
|
228
|
+
redirectUrl.searchParams.set("state", pending.clientState);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return Response.redirect(redirectUrl.toString(), 302);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.error("OAuth callback error:", err);
|
|
234
|
+
|
|
235
|
+
// Redirect back to client with error
|
|
236
|
+
const redirectUrl = new URL(pending.redirectUri);
|
|
237
|
+
redirectUrl.searchParams.set("error", "server_error");
|
|
238
|
+
redirectUrl.searchParams.set(
|
|
239
|
+
"error_description",
|
|
240
|
+
"Failed to exchange authorization code",
|
|
241
|
+
);
|
|
242
|
+
if (pending.clientState)
|
|
243
|
+
redirectUrl.searchParams.set("state", pending.clientState);
|
|
244
|
+
|
|
245
|
+
return Response.redirect(redirectUrl.toString(), 302);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Handle token exchange - decodes our code to get the actual token
|
|
251
|
+
* Stateless: token is encoded in the code
|
|
252
|
+
*/
|
|
253
|
+
const handleToken = async (req: Request): Promise<Response> => {
|
|
254
|
+
try {
|
|
255
|
+
const contentType = req.headers.get("content-type") ?? "";
|
|
256
|
+
let body: Record<string, string>;
|
|
257
|
+
|
|
258
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
259
|
+
const formData = await req.formData();
|
|
260
|
+
body = Object.fromEntries(formData.entries()) as Record<string, string>;
|
|
261
|
+
} else {
|
|
262
|
+
body = await req.json();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const { code, code_verifier, grant_type } = body;
|
|
266
|
+
|
|
267
|
+
if (grant_type !== "authorization_code") {
|
|
268
|
+
return Response.json(
|
|
269
|
+
{
|
|
270
|
+
error: "unsupported_grant_type",
|
|
271
|
+
error_description: "Only authorization_code supported",
|
|
272
|
+
},
|
|
273
|
+
{ status: 400 },
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!code) {
|
|
278
|
+
return Response.json(
|
|
279
|
+
{ error: "invalid_request", error_description: "code is required" },
|
|
280
|
+
{ status: 400 },
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Decode the code to get the token
|
|
285
|
+
const payload = decodeState<CodePayload>(code);
|
|
286
|
+
if (!payload || !payload.accessToken) {
|
|
287
|
+
return Response.json(
|
|
288
|
+
{
|
|
289
|
+
error: "invalid_grant",
|
|
290
|
+
error_description: "Invalid or expired code",
|
|
291
|
+
},
|
|
292
|
+
{ status: 400 },
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Verify PKCE if code challenge was provided
|
|
297
|
+
if (payload.codeChallenge) {
|
|
298
|
+
if (!code_verifier) {
|
|
299
|
+
return Response.json(
|
|
300
|
+
{
|
|
301
|
+
error: "invalid_grant",
|
|
302
|
+
error_description: "code_verifier required",
|
|
303
|
+
},
|
|
304
|
+
{ status: 400 },
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Verify the code verifier
|
|
309
|
+
let computedChallenge: string;
|
|
310
|
+
if (payload.codeChallengeMethod === "S256") {
|
|
311
|
+
const encoder = new TextEncoder();
|
|
312
|
+
const data = encoder.encode(code_verifier);
|
|
313
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
314
|
+
computedChallenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
|
|
315
|
+
.replace(/\+/g, "-")
|
|
316
|
+
.replace(/\//g, "_")
|
|
317
|
+
.replace(/=+$/, "");
|
|
318
|
+
} else {
|
|
319
|
+
computedChallenge = code_verifier;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (computedChallenge !== payload.codeChallenge) {
|
|
323
|
+
return Response.json(
|
|
324
|
+
{
|
|
325
|
+
error: "invalid_grant",
|
|
326
|
+
error_description: "Invalid code_verifier",
|
|
327
|
+
},
|
|
328
|
+
{ status: 400 },
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Return the actual token
|
|
334
|
+
return Response.json(
|
|
335
|
+
{
|
|
336
|
+
access_token: payload.accessToken,
|
|
337
|
+
token_type: payload.tokenType,
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
headers: {
|
|
341
|
+
"Cache-Control": "no-store",
|
|
342
|
+
Pragma: "no-cache",
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
console.error("Token exchange error:", err);
|
|
348
|
+
return Response.json(
|
|
349
|
+
{
|
|
350
|
+
error: "server_error",
|
|
351
|
+
error_description: "Failed to process token request",
|
|
352
|
+
},
|
|
353
|
+
{ status: 500 },
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Handle dynamic client registration (RFC7591)
|
|
360
|
+
* Stateless: just generates a client_id and returns it, no storage needed
|
|
361
|
+
*/
|
|
362
|
+
const handleClientRegistration = async (req: Request): Promise<Response> => {
|
|
363
|
+
try {
|
|
364
|
+
const body = (await req.json()) as {
|
|
365
|
+
redirect_uris?: string[];
|
|
366
|
+
client_name?: string;
|
|
367
|
+
grant_types?: string[];
|
|
368
|
+
response_types?: string[];
|
|
369
|
+
token_endpoint_auth_method?: string;
|
|
370
|
+
scope?: string;
|
|
371
|
+
client_uri?: string;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Validate redirect URIs
|
|
375
|
+
if (!body.redirect_uris || body.redirect_uris.length === 0) {
|
|
376
|
+
return Response.json(
|
|
377
|
+
{
|
|
378
|
+
error: "invalid_redirect_uri",
|
|
379
|
+
error_description: "At least one redirect_uri is required",
|
|
380
|
+
},
|
|
381
|
+
{ status: 400 },
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const uri of body.redirect_uris) {
|
|
386
|
+
if (!isValidRedirectUri(uri)) {
|
|
387
|
+
return Response.json(
|
|
388
|
+
{
|
|
389
|
+
error: "invalid_redirect_uri",
|
|
390
|
+
error_description: `Invalid redirect URI: ${uri}`,
|
|
391
|
+
},
|
|
392
|
+
{ status: 400 },
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const clientId = generateRandomToken(32);
|
|
398
|
+
const clientSecret =
|
|
399
|
+
body.token_endpoint_auth_method !== "none"
|
|
400
|
+
? generateRandomToken(32)
|
|
401
|
+
: undefined;
|
|
402
|
+
const now = Math.floor(Date.now() / 1000);
|
|
403
|
+
|
|
404
|
+
const client: OAuthClient = {
|
|
405
|
+
client_id: clientId,
|
|
406
|
+
client_secret: clientSecret,
|
|
407
|
+
client_name: body.client_name,
|
|
408
|
+
redirect_uris: body.redirect_uris,
|
|
409
|
+
grant_types: body.grant_types ?? ["authorization_code"],
|
|
410
|
+
response_types: body.response_types ?? ["code"],
|
|
411
|
+
token_endpoint_auth_method:
|
|
412
|
+
body.token_endpoint_auth_method ?? "client_secret_post",
|
|
413
|
+
scope: body.scope,
|
|
414
|
+
client_id_issued_at: now,
|
|
415
|
+
client_secret_expires_at: 0,
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// Save client if persistence is provided
|
|
419
|
+
if (oauth.persistence) {
|
|
420
|
+
await oauth.persistence.saveClient(client);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return new Response(JSON.stringify(client), {
|
|
424
|
+
status: 201,
|
|
425
|
+
headers: {
|
|
426
|
+
"Content-Type": "application/json",
|
|
427
|
+
"Cache-Control": "no-store",
|
|
428
|
+
Pragma: "no-cache",
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
} catch (err) {
|
|
432
|
+
console.error("Client registration error:", err);
|
|
433
|
+
return Response.json(
|
|
434
|
+
{
|
|
435
|
+
error: "invalid_client_metadata",
|
|
436
|
+
error_description: "Invalid client registration request",
|
|
437
|
+
},
|
|
438
|
+
{ status: 400 },
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Return 401 with WWW-Authenticate header for unauthenticated MCP requests
|
|
445
|
+
* Per MCP spec: MUST include resource_metadata URL
|
|
446
|
+
*/
|
|
447
|
+
const createUnauthorizedResponse = (req: Request): Response => {
|
|
448
|
+
const url = new URL(req.url);
|
|
449
|
+
const resourceMetadataUrl = `${url.origin}/.well-known/oauth-protected-resource`;
|
|
450
|
+
const wwwAuthenticateValue = `Bearer resource_metadata="${resourceMetadataUrl}", scope="*"`;
|
|
451
|
+
|
|
452
|
+
return Response.json(
|
|
453
|
+
{
|
|
454
|
+
jsonrpc: "2.0",
|
|
455
|
+
error: {
|
|
456
|
+
code: -32000,
|
|
457
|
+
message: "Unauthorized: Authentication required",
|
|
458
|
+
},
|
|
459
|
+
id: null,
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
status: 401,
|
|
463
|
+
headers: {
|
|
464
|
+
"WWW-Authenticate": wwwAuthenticateValue,
|
|
465
|
+
"Access-Control-Expose-Headers": "WWW-Authenticate",
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
);
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Check if request has authentication token
|
|
473
|
+
*/
|
|
474
|
+
const hasAuth = (req: Request): boolean => {
|
|
475
|
+
const authHeader = req.headers.get("Authorization");
|
|
476
|
+
const meshToken = req.headers.get("x-mesh-token");
|
|
477
|
+
return !!(authHeader || meshToken);
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
handleProtectedResourceMetadata,
|
|
482
|
+
handleAuthorizationServerMetadata,
|
|
483
|
+
handleAuthorize,
|
|
484
|
+
handleOAuthCallback,
|
|
485
|
+
handleToken,
|
|
486
|
+
handleClientRegistration,
|
|
487
|
+
createUnauthorizedResponse,
|
|
488
|
+
hasAuth,
|
|
489
|
+
};
|
|
490
|
+
}
|
package/src/proxy.ts
CHANGED
|
@@ -1,157 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { convertJsonSchemaToZod } from "zod-from-json-schema";
|
|
3
|
-
import { MCPConnection } from "./connection.ts";
|
|
4
|
-
import { createServerClient } from "./mcp-client.ts";
|
|
5
|
-
import type { CreateStubAPIOptions } from "./mcp.ts";
|
|
6
|
-
|
|
7
|
-
const safeParse = (content: string) => {
|
|
8
|
-
try {
|
|
9
|
-
return JSON.parse(content as string);
|
|
10
|
-
} catch {
|
|
11
|
-
return content;
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const toolsMap = new Map<
|
|
16
|
-
string,
|
|
17
|
-
Promise<
|
|
18
|
-
Array<{
|
|
19
|
-
name: string;
|
|
20
|
-
inputSchema: any;
|
|
21
|
-
outputSchema?: any;
|
|
22
|
-
description: string;
|
|
23
|
-
}>
|
|
24
|
-
>
|
|
25
|
-
>();
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The base fetcher used to fetch the MCP from API.
|
|
29
|
-
*/
|
|
30
|
-
export function createMCPClientProxy<T extends Record<string, unknown>>(
|
|
31
|
-
options: CreateStubAPIOptions,
|
|
32
|
-
): T {
|
|
33
|
-
const connection: MCPConnection = options.connection;
|
|
34
|
-
|
|
35
|
-
return new Proxy<T>({} as T, {
|
|
36
|
-
get(_, name) {
|
|
37
|
-
if (name === "toJSON") {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
if (typeof name !== "string") {
|
|
41
|
-
throw new Error("Name must be a string");
|
|
42
|
-
}
|
|
43
|
-
async function callToolFn(args: unknown) {
|
|
44
|
-
const debugId = options?.debugId?.();
|
|
45
|
-
const extraHeaders = debugId
|
|
46
|
-
? { "x-trace-debug-id": debugId }
|
|
47
|
-
: undefined;
|
|
48
|
-
|
|
49
|
-
// Create a connection with the tool name in the URL path for better logging
|
|
50
|
-
// Only modify connections that have a URL property (HTTP, SSE, Websocket)
|
|
51
|
-
// Use automatic detection based on URL, with optional override
|
|
52
|
-
|
|
53
|
-
const { client, callStreamableTool } = await createServerClient(
|
|
54
|
-
{ connection },
|
|
55
|
-
undefined,
|
|
56
|
-
extraHeaders,
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
if (options?.streamable?.[String(name)]) {
|
|
60
|
-
return callStreamableTool(String(name), args);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const { structuredContent, isError, content } = await client.callTool(
|
|
64
|
-
{
|
|
65
|
-
name: String(name),
|
|
66
|
-
arguments: args as Record<string, unknown>,
|
|
67
|
-
},
|
|
68
|
-
undefined,
|
|
69
|
-
{
|
|
70
|
-
timeout: 3000000,
|
|
71
|
-
},
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
if (isError) {
|
|
75
|
-
// @ts-expect-error - content is not typed
|
|
76
|
-
const maybeErrorMessage = content?.[0]?.text;
|
|
77
|
-
const error =
|
|
78
|
-
typeof maybeErrorMessage === "string"
|
|
79
|
-
? safeParse(maybeErrorMessage)
|
|
80
|
-
: null;
|
|
81
|
-
|
|
82
|
-
const throwableError =
|
|
83
|
-
error?.code && typeof options?.getErrorByStatusCode === "function"
|
|
84
|
-
? options.getErrorByStatusCode(
|
|
85
|
-
error.code,
|
|
86
|
-
error.message,
|
|
87
|
-
error.traceId,
|
|
88
|
-
)
|
|
89
|
-
: null;
|
|
90
|
-
|
|
91
|
-
if (throwableError) {
|
|
92
|
-
throw throwableError;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
throw new Error(
|
|
96
|
-
`Tool ${String(name)} returned an error: ${JSON.stringify(
|
|
97
|
-
structuredContent ?? content,
|
|
98
|
-
)}`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
return structuredContent;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const listToolsFn = async () => {
|
|
105
|
-
const { client } = await createServerClient({ connection });
|
|
106
|
-
const { tools } = await client.listTools();
|
|
107
|
-
|
|
108
|
-
return tools as {
|
|
109
|
-
name: string;
|
|
110
|
-
inputSchema: any;
|
|
111
|
-
outputSchema?: any;
|
|
112
|
-
description: string;
|
|
113
|
-
}[];
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
async function listToolsOnce() {
|
|
117
|
-
const conn = connection;
|
|
118
|
-
const key = JSON.stringify(conn);
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
if (!toolsMap.has(key)) {
|
|
122
|
-
toolsMap.set(key, listToolsFn());
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return await toolsMap.get(key)!;
|
|
126
|
-
} catch (error) {
|
|
127
|
-
console.error("Failed to list tools", error);
|
|
128
|
-
|
|
129
|
-
toolsMap.delete(key);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
callToolFn.asTool = async () => {
|
|
134
|
-
const tools = (await listToolsOnce()) ?? [];
|
|
135
|
-
const tool = tools.find((t) => t.name === name);
|
|
136
|
-
if (!tool) {
|
|
137
|
-
throw new Error(`Tool ${name} not found`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
...tool,
|
|
142
|
-
id: tool.name,
|
|
143
|
-
inputSchema: tool.inputSchema
|
|
144
|
-
? convertJsonSchemaToZod(tool.inputSchema)
|
|
145
|
-
: undefined,
|
|
146
|
-
outputSchema: tool.outputSchema
|
|
147
|
-
? convertJsonSchemaToZod(tool.outputSchema)
|
|
148
|
-
: undefined,
|
|
149
|
-
execute: (input: any) => {
|
|
150
|
-
return callToolFn(input.context);
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
};
|
|
154
|
-
return callToolFn;
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
}
|
|
1
|
+
export * from "@decocms/bindings/client";
|
package/src/tools.ts
CHANGED
|
@@ -149,11 +149,76 @@ export interface OnChangeCallback<TSchema extends z.ZodTypeAny = never> {
|
|
|
149
149
|
state: z.infer<TSchema>;
|
|
150
150
|
scopes: string[];
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
export interface OAuthParams {
|
|
154
|
+
code: string;
|
|
155
|
+
code_verifier?: string;
|
|
156
|
+
code_challenge_method?: "S256" | "plain";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface OAuthTokenResponse {
|
|
160
|
+
access_token: string;
|
|
161
|
+
token_type: string;
|
|
162
|
+
expires_in?: number;
|
|
163
|
+
refresh_token?: string;
|
|
164
|
+
scope?: string;
|
|
165
|
+
[key: string]: unknown;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* OAuth client for dynamic client registration (RFC7591)
|
|
170
|
+
*/
|
|
171
|
+
export interface OAuthClient {
|
|
172
|
+
client_id: string;
|
|
173
|
+
client_secret?: string;
|
|
174
|
+
client_name?: string;
|
|
175
|
+
redirect_uris: string[];
|
|
176
|
+
grant_types?: string[];
|
|
177
|
+
response_types?: string[];
|
|
178
|
+
token_endpoint_auth_method?: string;
|
|
179
|
+
scope?: string;
|
|
180
|
+
client_id_issued_at?: number;
|
|
181
|
+
client_secret_expires_at?: number;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* OAuth configuration for MCP servers implementing PKCE flow
|
|
186
|
+
* Per MCP Authorization spec: https://modelcontextprotocol.io/specification/draft/basic/authorization
|
|
187
|
+
*/
|
|
188
|
+
export interface OAuthConfig {
|
|
189
|
+
mode: "PKCE";
|
|
190
|
+
/**
|
|
191
|
+
* The external authorization server URL (e.g., "https://openrouter.ai")
|
|
192
|
+
* Used in protected resource metadata to indicate where clients should authenticate
|
|
193
|
+
*/
|
|
194
|
+
authorizationServer: string;
|
|
195
|
+
/**
|
|
196
|
+
* Generates the authorization URL where users should be redirected
|
|
197
|
+
* @param callbackUrl - The URL the OAuth provider will redirect back to with the code
|
|
198
|
+
* @returns The full authorization URL to redirect the user to
|
|
199
|
+
*/
|
|
200
|
+
authorizationUrl: (callbackUrl: string) => string;
|
|
201
|
+
/**
|
|
202
|
+
* Exchanges the authorization code for access tokens
|
|
203
|
+
* Called when the OAuth callback is received with a code
|
|
204
|
+
*/
|
|
205
|
+
exchangeCode: (oauthParams: OAuthParams) => Promise<OAuthTokenResponse>;
|
|
206
|
+
/**
|
|
207
|
+
* Optional: persistence for dynamic client registration (RFC7591)
|
|
208
|
+
* If not provided, clients are accepted without validation
|
|
209
|
+
*/
|
|
210
|
+
persistence?: {
|
|
211
|
+
getClient: (clientId: string) => Promise<OAuthClient | null>;
|
|
212
|
+
saveClient: (client: OAuthClient) => Promise<void>;
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
152
216
|
export interface CreateMCPServerOptions<
|
|
153
217
|
Env = unknown,
|
|
154
218
|
TSchema extends z.ZodTypeAny = never,
|
|
155
219
|
> {
|
|
156
220
|
before?: (env: Env & DefaultEnv<TSchema>) => Promise<void> | void;
|
|
221
|
+
oauth?: OAuthConfig;
|
|
157
222
|
configuration?: {
|
|
158
223
|
onChange?: (
|
|
159
224
|
env: Env & DefaultEnv<TSchema>,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { HTTPClientTransport } from "@decocms/bindings/client";
|
package/src/mcp-client.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Client as BaseClient,
|
|
3
|
-
ClientOptions,
|
|
4
|
-
} from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
|
-
import {
|
|
6
|
-
SSEClientTransport,
|
|
7
|
-
SSEClientTransportOptions,
|
|
8
|
-
} from "@modelcontextprotocol/sdk/client/sse.js";
|
|
9
|
-
import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/websocket.js";
|
|
10
|
-
import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
|
11
|
-
import {
|
|
12
|
-
Implementation,
|
|
13
|
-
ListToolsRequest,
|
|
14
|
-
ListToolsResultSchema,
|
|
15
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
16
|
-
import { MCPConnection } from "./connection.ts";
|
|
17
|
-
import { HTTPClientTransport } from "./http-client-transport.ts";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* WARNNING: This is a hack to prevent schema compilation errors.
|
|
21
|
-
* More info at: https://github.com/modelcontextprotocol/typescript-sdk/issues/923
|
|
22
|
-
*
|
|
23
|
-
* Make sure to keep this updated with the right version of the SDK.
|
|
24
|
-
* https://github.com/modelcontextprotocol/typescript-sdk/blob/bf817939917277a4c59f2e19e7b44b8dd7ff140c/src/client/index.ts#L480
|
|
25
|
-
*/
|
|
26
|
-
class Client extends BaseClient {
|
|
27
|
-
constructor(_clientInfo: Implementation, options?: ClientOptions) {
|
|
28
|
-
super(_clientInfo, options);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
override async listTools(
|
|
32
|
-
params?: ListToolsRequest["params"],
|
|
33
|
-
options?: RequestOptions,
|
|
34
|
-
) {
|
|
35
|
-
const result = await this.request(
|
|
36
|
-
{ method: "tools/list", params },
|
|
37
|
-
ListToolsResultSchema,
|
|
38
|
-
options,
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
return result;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface ServerClient {
|
|
46
|
-
client: Client;
|
|
47
|
-
callStreamableTool: (tool: string, args: unknown) => Promise<Response>;
|
|
48
|
-
}
|
|
49
|
-
export const createServerClient = async (
|
|
50
|
-
mcpServer: { connection: MCPConnection; name?: string },
|
|
51
|
-
signal?: AbortSignal,
|
|
52
|
-
extraHeaders?: Record<string, string>,
|
|
53
|
-
): Promise<ServerClient> => {
|
|
54
|
-
const transport = createTransport(mcpServer.connection, signal, extraHeaders);
|
|
55
|
-
|
|
56
|
-
if (!transport) {
|
|
57
|
-
throw new Error("Unknown MCP connection type");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const client = new Client({
|
|
61
|
-
name: mcpServer?.name ?? "MCP Client",
|
|
62
|
-
version: "1.0.0",
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
await client.connect(transport);
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
client,
|
|
69
|
-
callStreamableTool: (tool, args) => {
|
|
70
|
-
if (mcpServer.connection.type !== "HTTP") {
|
|
71
|
-
throw new Error("HTTP connection required");
|
|
72
|
-
}
|
|
73
|
-
return fetch(mcpServer.connection.url + `/call-tool/${tool}`, {
|
|
74
|
-
method: "POST",
|
|
75
|
-
redirect: "manual",
|
|
76
|
-
body: JSON.stringify(args),
|
|
77
|
-
headers: {
|
|
78
|
-
...extraHeaders,
|
|
79
|
-
Authorization: `Bearer ${mcpServer.connection.token}`,
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export const createTransport = (
|
|
87
|
-
connection: MCPConnection,
|
|
88
|
-
signal?: AbortSignal,
|
|
89
|
-
extraHeaders?: Record<string, string>,
|
|
90
|
-
) => {
|
|
91
|
-
if (connection.type === "Websocket") {
|
|
92
|
-
return new WebSocketClientTransport(new URL(connection.url));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (connection.type !== "SSE" && connection.type !== "HTTP") {
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const authHeaders: Record<string, string> = connection.token
|
|
100
|
-
? { authorization: `Bearer ${connection.token}` }
|
|
101
|
-
: {};
|
|
102
|
-
|
|
103
|
-
const headers: Record<string, string> = {
|
|
104
|
-
...authHeaders,
|
|
105
|
-
...(extraHeaders ?? {}),
|
|
106
|
-
...("headers" in connection ? connection.headers || {} : {}),
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
if (connection.type === "SSE") {
|
|
110
|
-
const config: SSEClientTransportOptions = {
|
|
111
|
-
requestInit: { headers, signal },
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
if (connection.token) {
|
|
115
|
-
config.eventSourceInit = {
|
|
116
|
-
fetch: (req, init) => {
|
|
117
|
-
return fetch(req, {
|
|
118
|
-
...init,
|
|
119
|
-
headers: {
|
|
120
|
-
...headers,
|
|
121
|
-
Accept: "text/event-stream",
|
|
122
|
-
},
|
|
123
|
-
signal,
|
|
124
|
-
});
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return new SSEClientTransport(new URL(connection.url), config);
|
|
130
|
-
}
|
|
131
|
-
return new HTTPClientTransport(new URL(connection.url), {
|
|
132
|
-
requestInit: {
|
|
133
|
-
headers,
|
|
134
|
-
signal,
|
|
135
|
-
// @ts-ignore - this is a valid option for fetch
|
|
136
|
-
credentials: "include",
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
};
|