@smithery/sdk 1.7.2 → 1.7.3
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/index.d.ts +11 -0
- package/dist/index.js +19 -0
- package/dist/openai/index.d.ts +2 -0
- package/dist/openai/index.js +1 -0
- package/dist/openai/widget.d.ts +67 -0
- package/dist/openai/widget.js +126 -0
- package/dist/react/ErrorBoundary.d.ts +18 -0
- package/dist/react/ErrorBoundary.js +28 -0
- package/dist/react/hooks.d.ts +32 -0
- package/dist/react/hooks.js +135 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.js +2 -0
- package/dist/react/types.d.ts +66 -0
- package/dist/react/types.js +1 -0
- package/dist/server/auth/identity.d.ts +18 -0
- package/dist/server/auth/identity.js +55 -0
- package/dist/server/auth/oauth.d.ts +21 -0
- package/dist/server/auth/oauth.js +155 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.js +6 -0
- package/dist/server/logger.d.ts +19 -0
- package/dist/server/logger.js +76 -0
- package/dist/server/session.d.ts +17 -0
- package/dist/server/session.js +36 -0
- package/dist/server/stateful.d.ts +48 -0
- package/dist/server/stateful.js +176 -0
- package/dist/server/stateless.d.ts +46 -0
- package/dist/server/stateless.js +123 -0
- package/dist/server/widget.d.ts +6 -0
- package/dist/server/widget.js +7 -0
- package/dist/shared/config.d.ts +42 -0
- package/dist/shared/config.js +133 -0
- package/dist/shared/patch.d.ts +12 -0
- package/dist/shared/patch.js +12 -0
- package/package.json +3 -2
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { authorizationHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/authorize.js";
|
|
2
|
+
import { metadataHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/metadata.js";
|
|
3
|
+
import { clientRegistrationHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/register.js";
|
|
4
|
+
import { revocationHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/revoke.js";
|
|
5
|
+
import { tokenHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/token.js";
|
|
6
|
+
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
|
|
7
|
+
import { createOAuthMetadata, mcpAuthMetadataRouter, } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
8
|
+
import { mountIdentity } from "./identity.js";
|
|
9
|
+
function isOAuthProvider(provider) {
|
|
10
|
+
return !!provider && "authorize" in provider;
|
|
11
|
+
}
|
|
12
|
+
export function mountOAuth(app, opts) {
|
|
13
|
+
// Determine base path once based on OAuth provider or identity
|
|
14
|
+
const provider = opts.provider;
|
|
15
|
+
const hasOAuth = isOAuthProvider(provider);
|
|
16
|
+
const rawBasePath = hasOAuth
|
|
17
|
+
? (provider.basePath ?? "/")
|
|
18
|
+
: (opts.identity?.basePath ?? "/");
|
|
19
|
+
const basePath = rawBasePath.endsWith("/") ? rawBasePath : `${rawBasePath}/`;
|
|
20
|
+
// Precompute endpoint pathnames from metadata
|
|
21
|
+
let authorizationPath;
|
|
22
|
+
let tokenPath;
|
|
23
|
+
let registrationPath;
|
|
24
|
+
let revocationPath;
|
|
25
|
+
if (isOAuthProvider(provider)) {
|
|
26
|
+
const placeholderIssuer = new URL("https://localhost");
|
|
27
|
+
const placeholderBaseUrl = new URL(basePath, placeholderIssuer);
|
|
28
|
+
const localMetadata = createOAuthMetadata({
|
|
29
|
+
provider,
|
|
30
|
+
issuerUrl: placeholderIssuer,
|
|
31
|
+
baseUrl: placeholderBaseUrl,
|
|
32
|
+
});
|
|
33
|
+
authorizationPath = new URL(localMetadata.authorization_endpoint).pathname;
|
|
34
|
+
tokenPath = new URL(localMetadata.token_endpoint).pathname;
|
|
35
|
+
if (localMetadata.registration_endpoint) {
|
|
36
|
+
registrationPath = new URL(localMetadata.registration_endpoint).pathname;
|
|
37
|
+
}
|
|
38
|
+
if (localMetadata.revocation_endpoint) {
|
|
39
|
+
revocationPath = new URL(localMetadata.revocation_endpoint).pathname;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Metadata endpoints
|
|
43
|
+
if (isOAuthProvider(provider)) {
|
|
44
|
+
// Mount a per-request adapter so issuer/baseUrl reflect Host/Proto
|
|
45
|
+
app.use((req, res, next) => {
|
|
46
|
+
if (!req.path.startsWith("/.well-known/"))
|
|
47
|
+
return next();
|
|
48
|
+
const host = req.get("host") ?? "localhost";
|
|
49
|
+
if (req.protocol !== "https") {
|
|
50
|
+
console.warn("Detected http but using https for issuer URL in OAuth metadata since it will fail otherwise.");
|
|
51
|
+
}
|
|
52
|
+
const issuerUrl = new URL(`https://${host}`);
|
|
53
|
+
const baseUrl = new URL(basePath, issuerUrl);
|
|
54
|
+
const oauthMetadata = createOAuthMetadata({
|
|
55
|
+
provider,
|
|
56
|
+
issuerUrl,
|
|
57
|
+
baseUrl,
|
|
58
|
+
});
|
|
59
|
+
if (opts.identity) {
|
|
60
|
+
oauthMetadata.grant_types_supported = Array.from(new Set([
|
|
61
|
+
...(oauthMetadata.grant_types_supported ?? []),
|
|
62
|
+
"urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
63
|
+
]));
|
|
64
|
+
}
|
|
65
|
+
const resourceServerUrl = new URL("/mcp", issuerUrl);
|
|
66
|
+
const metadataRouter = mcpAuthMetadataRouter({
|
|
67
|
+
oauthMetadata,
|
|
68
|
+
resourceServerUrl,
|
|
69
|
+
});
|
|
70
|
+
return metadataRouter(req, res, next);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else if (opts.identity) {
|
|
74
|
+
// Identity-only: explicitly mount protected resource metadata endpoint
|
|
75
|
+
app.use("/.well-known/oauth-protected-resource", (req, res, next) => {
|
|
76
|
+
const host = req.get("host") ?? "localhost";
|
|
77
|
+
const issuerUrl = new URL(`https://${host}`);
|
|
78
|
+
const protectedResourceMetadata = {
|
|
79
|
+
resource: new URL("/mcp", issuerUrl).href,
|
|
80
|
+
authorization_servers: [issuerUrl.href],
|
|
81
|
+
};
|
|
82
|
+
return metadataHandler(protectedResourceMetadata)(req, res, next);
|
|
83
|
+
});
|
|
84
|
+
// Identity-only: also advertise minimal AS metadata for discovery per RFC 8414
|
|
85
|
+
app.use("/.well-known/oauth-authorization-server", (req, res, next) => {
|
|
86
|
+
const host = req.get("host") ?? "localhost";
|
|
87
|
+
const issuerUrl = new URL(`https://${host}`);
|
|
88
|
+
const oauthMetadata = {
|
|
89
|
+
issuer: issuerUrl.href,
|
|
90
|
+
token_endpoint: new URL(`${basePath}token`, issuerUrl).href,
|
|
91
|
+
grant_types_supported: ["urn:ietf:params:oauth:grant-type:jwt-bearer"],
|
|
92
|
+
};
|
|
93
|
+
return metadataHandler(oauthMetadata)(req, res, next);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// Mount identity (JWT bearer grant) first so OAuth token can fall through
|
|
97
|
+
if (opts.identity) {
|
|
98
|
+
const identityOptions = {
|
|
99
|
+
...opts.identity,
|
|
100
|
+
basePath,
|
|
101
|
+
tokenPath: tokenPath ?? `${basePath}token`,
|
|
102
|
+
};
|
|
103
|
+
mountIdentity(app, identityOptions);
|
|
104
|
+
}
|
|
105
|
+
// Mount OAuth endpoints functionally if an OAuth provider is present
|
|
106
|
+
if (isOAuthProvider(provider)) {
|
|
107
|
+
// Authorization endpoint
|
|
108
|
+
const authPath = authorizationPath ?? `${basePath}authorize`;
|
|
109
|
+
app.use(authPath, authorizationHandler({ provider }));
|
|
110
|
+
// Token endpoint (OAuth); identity's token handler will handle JWT grant and call next() otherwise
|
|
111
|
+
const tokPath = tokenPath ?? `${basePath}token`;
|
|
112
|
+
app.use(tokPath, tokenHandler({ provider }));
|
|
113
|
+
// Dynamic client registration if supported
|
|
114
|
+
if (provider.clientsStore?.registerClient) {
|
|
115
|
+
const regPath = registrationPath ?? `${basePath}register`;
|
|
116
|
+
app.use(regPath, clientRegistrationHandler({ clientsStore: provider.clientsStore }));
|
|
117
|
+
}
|
|
118
|
+
// Token revocation if supported
|
|
119
|
+
if (provider.revokeToken) {
|
|
120
|
+
const revPath = revocationPath ?? `${basePath}revoke`;
|
|
121
|
+
app.use(revPath, revocationHandler({ provider }));
|
|
122
|
+
}
|
|
123
|
+
// Optional OAuth callback
|
|
124
|
+
const callbackHandler = provider.handleOAuthCallback?.bind(provider);
|
|
125
|
+
if (callbackHandler) {
|
|
126
|
+
const callbackPath = provider.callbackPath ?? "/callback";
|
|
127
|
+
app.get(callbackPath, async (req, res) => {
|
|
128
|
+
const code = typeof req.query.code === "string" ? req.query.code : undefined;
|
|
129
|
+
const state = typeof req.query.state === "string" ? req.query.state : undefined;
|
|
130
|
+
if (!code) {
|
|
131
|
+
res.status(400).send("Invalid request parameters");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const redirectUrl = await callbackHandler(code, state, res);
|
|
136
|
+
res.redirect(redirectUrl.toString());
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
console.error(error);
|
|
140
|
+
res.status(500).send("Error during authentication callback");
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Protect MCP resource with bearer auth if a verifier/provider is present
|
|
146
|
+
if (provider) {
|
|
147
|
+
app.use("/mcp", (req, res, next) => {
|
|
148
|
+
return requireBearerAuth({
|
|
149
|
+
verifier: provider,
|
|
150
|
+
requiredScopes: provider.requiredScopes,
|
|
151
|
+
resourceMetadataUrl: provider.resourceMetadataUrl,
|
|
152
|
+
})(req, res, next);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./stateful.js";
|
|
2
|
+
export * from "./stateless.js";
|
|
3
|
+
export * from "./session.js";
|
|
4
|
+
export * from "./auth/oauth.js";
|
|
5
|
+
export * from "./auth/identity.js";
|
|
6
|
+
export { createWidgetServer } from "./widget.js";
|
|
7
|
+
export type { WidgetServerOptions } from "./widget.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger interface for structured logging
|
|
3
|
+
*/
|
|
4
|
+
export interface Logger {
|
|
5
|
+
info(msg: string, ...args: unknown[]): void;
|
|
6
|
+
info(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
|
|
7
|
+
error(msg: string, ...args: unknown[]): void;
|
|
8
|
+
error(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
|
|
9
|
+
warn(msg: string, ...args: unknown[]): void;
|
|
10
|
+
warn(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
|
|
11
|
+
debug(msg: string, ...args: unknown[]): void;
|
|
12
|
+
debug(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
|
|
13
|
+
}
|
|
14
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
15
|
+
/**
|
|
16
|
+
* Creates a simple console-based logger with pretty formatting
|
|
17
|
+
*/
|
|
18
|
+
export declare function createLogger(logLevel?: LogLevel): Logger;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight stringify with depth limiting
|
|
4
|
+
*/
|
|
5
|
+
function stringifyWithDepth(obj, maxDepth = 3) {
|
|
6
|
+
let depth = 0;
|
|
7
|
+
const seen = new WeakSet();
|
|
8
|
+
try {
|
|
9
|
+
return JSON.stringify(obj, (key, value) => {
|
|
10
|
+
// Track depth
|
|
11
|
+
if (key === "")
|
|
12
|
+
depth = 0;
|
|
13
|
+
else if (typeof value === "object" && value !== null)
|
|
14
|
+
depth++;
|
|
15
|
+
// Depth limit
|
|
16
|
+
if (depth > maxDepth) {
|
|
17
|
+
return "[Object]";
|
|
18
|
+
}
|
|
19
|
+
// Circular reference check
|
|
20
|
+
if (typeof value === "object" && value !== null) {
|
|
21
|
+
if (seen.has(value))
|
|
22
|
+
return "[Circular]";
|
|
23
|
+
seen.add(value);
|
|
24
|
+
}
|
|
25
|
+
// Handle special types
|
|
26
|
+
if (typeof value === "function")
|
|
27
|
+
return "[Function]";
|
|
28
|
+
if (typeof value === "bigint")
|
|
29
|
+
return `${value}n`;
|
|
30
|
+
if (value instanceof Error)
|
|
31
|
+
return { name: value.name, message: value.message };
|
|
32
|
+
if (value instanceof Date)
|
|
33
|
+
return value.toISOString();
|
|
34
|
+
return value;
|
|
35
|
+
}, 2);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return String(obj);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Creates a simple console-based logger with pretty formatting
|
|
43
|
+
*/
|
|
44
|
+
export function createLogger(logLevel = "info") {
|
|
45
|
+
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
46
|
+
const currentLevel = levels[logLevel];
|
|
47
|
+
const formatLog = (level, color, msgOrObj, msg) => {
|
|
48
|
+
const time = new Date().toISOString().split("T")[1].split(".")[0];
|
|
49
|
+
const timestamp = chalk.dim(time);
|
|
50
|
+
const levelStr = color(level);
|
|
51
|
+
if (typeof msgOrObj === "string") {
|
|
52
|
+
return `${timestamp} ${levelStr} ${msgOrObj}`;
|
|
53
|
+
}
|
|
54
|
+
const message = msg || "";
|
|
55
|
+
const data = stringifyWithDepth(msgOrObj, 3);
|
|
56
|
+
return `${timestamp} ${levelStr} ${message}\n${chalk.dim(data)}`;
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
debug: (msgOrObj, msg) => {
|
|
60
|
+
if (currentLevel <= 0)
|
|
61
|
+
console.error(formatLog("DEBUG", chalk.cyan, msgOrObj, msg));
|
|
62
|
+
},
|
|
63
|
+
info: (msgOrObj, msg) => {
|
|
64
|
+
if (currentLevel <= 1)
|
|
65
|
+
console.error(formatLog("INFO", chalk.blue, msgOrObj, msg));
|
|
66
|
+
},
|
|
67
|
+
warn: (msgOrObj, msg) => {
|
|
68
|
+
if (currentLevel <= 2)
|
|
69
|
+
console.error(formatLog("WARN", chalk.yellow, msgOrObj, msg));
|
|
70
|
+
},
|
|
71
|
+
error: (msgOrObj, msg) => {
|
|
72
|
+
if (currentLevel <= 3)
|
|
73
|
+
console.error(formatLog("ERROR", chalk.red, msgOrObj, msg));
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
2
|
+
export interface SessionStore<T extends Transport> {
|
|
3
|
+
/** return existing transport (or `undefined`) */
|
|
4
|
+
get(id: string): T | undefined;
|
|
5
|
+
/** insert / update */
|
|
6
|
+
set(id: string, t: T): void;
|
|
7
|
+
/** optional - explicit eviction */
|
|
8
|
+
delete?(id: string): void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Minimal Map‑based LRU implementation that fulfils {@link SessionStore}.
|
|
12
|
+
* Keeps at most `max` transports; upon insert, the least‑recently‑used entry
|
|
13
|
+
* (oldest insertion order) is removed and the evicted transport is closed.
|
|
14
|
+
*
|
|
15
|
+
* @param max maximum number of sessions to retain (default = 1000)
|
|
16
|
+
*/
|
|
17
|
+
export declare const createLRUStore: <T extends Transport>(max?: number) => SessionStore<T>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Map‑based LRU implementation that fulfils {@link SessionStore}.
|
|
3
|
+
* Keeps at most `max` transports; upon insert, the least‑recently‑used entry
|
|
4
|
+
* (oldest insertion order) is removed and the evicted transport is closed.
|
|
5
|
+
*
|
|
6
|
+
* @param max maximum number of sessions to retain (default = 1000)
|
|
7
|
+
*/
|
|
8
|
+
export const createLRUStore = (max = 1000) => {
|
|
9
|
+
// ECMA‑262 §23.1.3.13 - the order of keys in a Map object is the order of insertion; operations that remove a key drop it from that order, and set appends when the key is new or has just been removed.
|
|
10
|
+
const cache = new Map();
|
|
11
|
+
return {
|
|
12
|
+
get: id => {
|
|
13
|
+
const t = cache.get(id);
|
|
14
|
+
if (!t)
|
|
15
|
+
return undefined;
|
|
16
|
+
// refresh position
|
|
17
|
+
cache.delete(id);
|
|
18
|
+
cache.set(id, t);
|
|
19
|
+
return t;
|
|
20
|
+
},
|
|
21
|
+
set: (id, transport) => {
|
|
22
|
+
if (cache.has(id)) {
|
|
23
|
+
// key already present - refresh position
|
|
24
|
+
cache.delete(id);
|
|
25
|
+
}
|
|
26
|
+
else if (cache.size >= max) {
|
|
27
|
+
// evict oldest entry (first in insertion order)
|
|
28
|
+
const [lruId, lruTransport] = cache.entries().next().value;
|
|
29
|
+
lruTransport.close?.();
|
|
30
|
+
cache.delete(lruId);
|
|
31
|
+
}
|
|
32
|
+
cache.set(id, transport);
|
|
33
|
+
},
|
|
34
|
+
delete: id => cache.delete(id),
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2
|
+
import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
|
|
3
|
+
import express from "express";
|
|
4
|
+
import type { z } from "zod";
|
|
5
|
+
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import { type SessionStore } from "./session.js";
|
|
7
|
+
import type { Logger } from "./logger.js";
|
|
8
|
+
/**
|
|
9
|
+
* Arguments when we create a new instance of your server
|
|
10
|
+
*/
|
|
11
|
+
export interface CreateServerArg<T = Record<string, unknown>> {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
config: T;
|
|
14
|
+
auth?: AuthInfo;
|
|
15
|
+
logger: Logger;
|
|
16
|
+
}
|
|
17
|
+
export type CreateServerFn<T = Record<string, unknown>> = (arg: CreateServerArg<T>) => Server;
|
|
18
|
+
/**
|
|
19
|
+
* Configuration options for the stateful server
|
|
20
|
+
*/
|
|
21
|
+
export interface StatefulServerOptions<T = Record<string, unknown>> {
|
|
22
|
+
/**
|
|
23
|
+
* Session store to use for managing active sessions
|
|
24
|
+
*/
|
|
25
|
+
sessionStore?: SessionStore<StreamableHTTPServerTransport>;
|
|
26
|
+
/**
|
|
27
|
+
* Zod schema for config validation
|
|
28
|
+
*/
|
|
29
|
+
schema?: z.ZodSchema<T>;
|
|
30
|
+
/**
|
|
31
|
+
* Express app instance to use (optional)
|
|
32
|
+
*/
|
|
33
|
+
app?: express.Application;
|
|
34
|
+
/**
|
|
35
|
+
* Log level for the server (default: 'info')
|
|
36
|
+
*/
|
|
37
|
+
logLevel?: "debug" | "info" | "warn" | "error";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Creates a stateful server for handling MCP requests.
|
|
41
|
+
* For every new session, we invoke createMcpServer to create a new instance of the server.
|
|
42
|
+
* @param createMcpServer Function to create an MCP server
|
|
43
|
+
* @param options Configuration options including optional schema validation and Express app
|
|
44
|
+
* @returns Express app
|
|
45
|
+
*/
|
|
46
|
+
export declare function createStatefulServer<T = Record<string, unknown>>(createMcpServer: CreateServerFn<T>, options?: StatefulServerOptions<T>): {
|
|
47
|
+
app: express.Application;
|
|
48
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import express from "express";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { parseAndValidateConfig } from "../shared/config.js";
|
|
6
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
7
|
+
import { createLRUStore } from "./session.js";
|
|
8
|
+
import { createLogger } from "./logger.js";
|
|
9
|
+
/**
|
|
10
|
+
* Creates a stateful server for handling MCP requests.
|
|
11
|
+
* For every new session, we invoke createMcpServer to create a new instance of the server.
|
|
12
|
+
* @param createMcpServer Function to create an MCP server
|
|
13
|
+
* @param options Configuration options including optional schema validation and Express app
|
|
14
|
+
* @returns Express app
|
|
15
|
+
*/
|
|
16
|
+
export function createStatefulServer(createMcpServer, options) {
|
|
17
|
+
const app = options?.app ?? express();
|
|
18
|
+
app.use("/mcp", express.json());
|
|
19
|
+
const sessionStore = options?.sessionStore ?? createLRUStore();
|
|
20
|
+
const logger = createLogger(options?.logLevel ?? "info");
|
|
21
|
+
// Handle POST requests for client-to-server communication
|
|
22
|
+
app.post("/mcp", async (req, res) => {
|
|
23
|
+
// Log incoming MCP request
|
|
24
|
+
logger.debug({
|
|
25
|
+
method: req.body.method,
|
|
26
|
+
id: req.body.id,
|
|
27
|
+
sessionId: req.headers["mcp-session-id"],
|
|
28
|
+
}, "MCP Request");
|
|
29
|
+
// Check for existing session ID
|
|
30
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
31
|
+
let transport;
|
|
32
|
+
if (sessionId && sessionStore.get(sessionId)) {
|
|
33
|
+
// Reuse existing transport
|
|
34
|
+
// biome-ignore lint/style/noNonNullAssertion: Not possible
|
|
35
|
+
transport = sessionStore.get(sessionId);
|
|
36
|
+
}
|
|
37
|
+
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
38
|
+
// New initialization request
|
|
39
|
+
const newSessionId = randomUUID();
|
|
40
|
+
transport = new StreamableHTTPServerTransport({
|
|
41
|
+
sessionIdGenerator: () => newSessionId,
|
|
42
|
+
onsessioninitialized: sessionId => {
|
|
43
|
+
// Store the transport by session ID
|
|
44
|
+
sessionStore.set(sessionId, transport);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
// Clean up transport when closed
|
|
48
|
+
transport.onclose = () => {
|
|
49
|
+
if (transport.sessionId) {
|
|
50
|
+
sessionStore.delete?.(transport.sessionId);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
// New session - validate config
|
|
54
|
+
const configResult = parseAndValidateConfig(req, options?.schema);
|
|
55
|
+
if (!configResult.ok) {
|
|
56
|
+
const status = configResult.error.status || 400;
|
|
57
|
+
logger.error({ error: configResult.error, sessionId: newSessionId }, "Config validation failed");
|
|
58
|
+
res.status(status).json(configResult.error);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const config = configResult.value;
|
|
62
|
+
try {
|
|
63
|
+
logger.info({ sessionId: newSessionId }, "Creating new session");
|
|
64
|
+
const server = createMcpServer({
|
|
65
|
+
sessionId: newSessionId,
|
|
66
|
+
config: config,
|
|
67
|
+
auth: req.auth,
|
|
68
|
+
logger,
|
|
69
|
+
});
|
|
70
|
+
// Connect to the MCP server
|
|
71
|
+
await server.connect(transport);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
logger.error({ error, sessionId: newSessionId }, "Error initializing server");
|
|
75
|
+
res.status(500).json({
|
|
76
|
+
jsonrpc: "2.0",
|
|
77
|
+
error: {
|
|
78
|
+
code: -32603,
|
|
79
|
+
message: "Error initializing server.",
|
|
80
|
+
},
|
|
81
|
+
id: null,
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// Invalid request
|
|
88
|
+
logger.warn({ sessionId }, "Session not found or expired");
|
|
89
|
+
res.status(400).json({
|
|
90
|
+
jsonrpc: "2.0",
|
|
91
|
+
error: {
|
|
92
|
+
code: -32000,
|
|
93
|
+
message: "Session not found or expired",
|
|
94
|
+
},
|
|
95
|
+
id: null,
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Handle the request
|
|
100
|
+
await transport.handleRequest(req, res, req.body);
|
|
101
|
+
// Log successful response
|
|
102
|
+
logger.debug({
|
|
103
|
+
method: req.body.method,
|
|
104
|
+
id: req.body.id,
|
|
105
|
+
sessionId: req.headers["mcp-session-id"],
|
|
106
|
+
}, "MCP Response sent");
|
|
107
|
+
});
|
|
108
|
+
// Add .well-known/mcp-config endpoint for configuration discovery
|
|
109
|
+
app.get("/.well-known/mcp-config", (req, res) => {
|
|
110
|
+
// Set proper content type for JSON Schema
|
|
111
|
+
res.set("Content-Type", "application/schema+json; charset=utf-8");
|
|
112
|
+
const baseSchema = options?.schema
|
|
113
|
+
? zodToJsonSchema(options.schema)
|
|
114
|
+
: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {},
|
|
117
|
+
required: [],
|
|
118
|
+
};
|
|
119
|
+
const configSchema = {
|
|
120
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
121
|
+
$id: `${req.protocol}://${req.get("host")}/.well-known/mcp-config`,
|
|
122
|
+
title: "MCP Session Configuration",
|
|
123
|
+
description: "Schema for the /mcp endpoint configuration",
|
|
124
|
+
"x-query-style": "dot+bracket",
|
|
125
|
+
...baseSchema,
|
|
126
|
+
};
|
|
127
|
+
res.json(configSchema);
|
|
128
|
+
});
|
|
129
|
+
// Handle GET requests for server-to-client notifications via SSE
|
|
130
|
+
app.get("/mcp", async (req, res) => {
|
|
131
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
132
|
+
if (!sessionId || !sessionStore.get(sessionId)) {
|
|
133
|
+
res.status(400).send("Invalid or expired session ID");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// biome-ignore lint/style/noNonNullAssertion: Not possible
|
|
137
|
+
const transport = sessionStore.get(sessionId);
|
|
138
|
+
await transport.handleRequest(req, res);
|
|
139
|
+
});
|
|
140
|
+
// Handle DELETE requests for session termination
|
|
141
|
+
// https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management
|
|
142
|
+
app.delete("/mcp", async (req, res) => {
|
|
143
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
144
|
+
if (!sessionId) {
|
|
145
|
+
logger.warn("Session termination request missing session ID");
|
|
146
|
+
res.status(400).json({
|
|
147
|
+
jsonrpc: "2.0",
|
|
148
|
+
error: {
|
|
149
|
+
code: -32600,
|
|
150
|
+
message: "Missing mcp-session-id header",
|
|
151
|
+
},
|
|
152
|
+
id: null,
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const transport = sessionStore.get(sessionId);
|
|
157
|
+
if (!transport) {
|
|
158
|
+
logger.warn({ sessionId }, "Session termination failed - not found");
|
|
159
|
+
res.status(404).json({
|
|
160
|
+
jsonrpc: "2.0",
|
|
161
|
+
error: {
|
|
162
|
+
code: -32000,
|
|
163
|
+
message: "Session not found or expired",
|
|
164
|
+
},
|
|
165
|
+
id: null,
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Close the transport
|
|
170
|
+
transport.close?.();
|
|
171
|
+
logger.info({ sessionId }, "Session terminated");
|
|
172
|
+
// Acknowledge session termination with 204 No Content
|
|
173
|
+
res.status(204).end();
|
|
174
|
+
});
|
|
175
|
+
return { app };
|
|
176
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
|
|
5
|
+
import type { OAuthMountOptions } from "./auth/oauth.js";
|
|
6
|
+
import { type Logger } from "./logger.js";
|
|
7
|
+
export type { Logger } from "./logger.js";
|
|
8
|
+
/**
|
|
9
|
+
* Arguments when we create a stateless server instance
|
|
10
|
+
*/
|
|
11
|
+
export interface CreateStatelessServerArg<T = Record<string, unknown>> {
|
|
12
|
+
config: T;
|
|
13
|
+
auth?: AuthInfo;
|
|
14
|
+
logger: Logger;
|
|
15
|
+
}
|
|
16
|
+
export type CreateStatelessServerFn<T = Record<string, unknown>> = (arg: CreateStatelessServerArg<T>) => Server;
|
|
17
|
+
/**
|
|
18
|
+
* Configuration options for the stateless server
|
|
19
|
+
*/
|
|
20
|
+
export interface StatelessServerOptions<T = Record<string, unknown>> {
|
|
21
|
+
/**
|
|
22
|
+
* Zod schema for config validation
|
|
23
|
+
*/
|
|
24
|
+
schema?: z.ZodSchema<T>;
|
|
25
|
+
/**
|
|
26
|
+
* Express app instance to use (optional)
|
|
27
|
+
*/
|
|
28
|
+
app?: express.Application;
|
|
29
|
+
oauth?: OAuthMountOptions;
|
|
30
|
+
/**
|
|
31
|
+
* Log level for the server (default: 'info')
|
|
32
|
+
*/
|
|
33
|
+
logLevel?: "debug" | "info" | "warn" | "error";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates a stateless server for handling MCP requests.
|
|
37
|
+
* Each request creates a new server instance - no session state is maintained.
|
|
38
|
+
* This is ideal for stateless API integrations and serverless environments.
|
|
39
|
+
*
|
|
40
|
+
* @param createMcpServer Function to create an MCP server
|
|
41
|
+
* @param options Configuration options including optional schema validation and Express app
|
|
42
|
+
* @returns Express app
|
|
43
|
+
*/
|
|
44
|
+
export declare function createStatelessServer<T = Record<string, unknown>>(createMcpServer: CreateStatelessServerFn<T>, options?: StatelessServerOptions<T>): {
|
|
45
|
+
app: express.Application;
|
|
46
|
+
};
|