@smithery/sdk 1.5.5 → 1.5.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/server/index.d.ts +3 -0
- package/dist/server/index.js +3 -0
- package/dist/server/stateful.js +0 -1
- package/dist/server/stateless.d.ts +35 -0
- package/dist/server/stateless.js +107 -0
- package/dist/shared/config.d.ts +9 -10
- package/dist/shared/config.js +68 -27
- package/package.json +1 -1
package/dist/server/stateful.js
CHANGED
|
@@ -102,7 +102,6 @@ export function createStatefulServer(createMcpServer, options) {
|
|
|
102
102
|
$id: `${req.protocol}://${req.get("host")}/.well-known/mcp-config`,
|
|
103
103
|
title: "MCP Session Configuration",
|
|
104
104
|
description: "Schema for the /mcp endpoint configuration",
|
|
105
|
-
"x-mcp-version": "1.0",
|
|
106
105
|
"x-query-style": "dot+bracket",
|
|
107
106
|
...baseSchema,
|
|
108
107
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Arguments when we create a stateless server instance
|
|
6
|
+
*/
|
|
7
|
+
export interface CreateStatelessServerArg<T = Record<string, unknown>> {
|
|
8
|
+
config: T;
|
|
9
|
+
}
|
|
10
|
+
export type CreateStatelessServerFn<T = Record<string, unknown>> = (arg: CreateStatelessServerArg<T>) => Server;
|
|
11
|
+
/**
|
|
12
|
+
* Configuration options for the stateless server
|
|
13
|
+
*/
|
|
14
|
+
export interface StatelessServerOptions<T = Record<string, unknown>> {
|
|
15
|
+
/**
|
|
16
|
+
* Zod schema for config validation
|
|
17
|
+
*/
|
|
18
|
+
schema?: z.ZodSchema<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Express app instance to use (optional)
|
|
21
|
+
*/
|
|
22
|
+
app?: express.Application;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Creates a stateless server for handling MCP requests.
|
|
26
|
+
* Each request creates a new server instance - no session state is maintained.
|
|
27
|
+
* This is ideal for stateless API integrations and serverless environments.
|
|
28
|
+
*
|
|
29
|
+
* @param createMcpServer Function to create an MCP server
|
|
30
|
+
* @param options Configuration options including optional schema validation and Express app
|
|
31
|
+
* @returns Express app
|
|
32
|
+
*/
|
|
33
|
+
export declare function createStatelessServer<T = Record<string, unknown>>(createMcpServer: CreateStatelessServerFn<T>, options?: StatelessServerOptions<T>): {
|
|
34
|
+
app: express.Application;
|
|
35
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import { parseAndValidateConfig } from "../shared/config.js";
|
|
4
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
5
|
+
/**
|
|
6
|
+
* Creates a stateless server for handling MCP requests.
|
|
7
|
+
* Each request creates a new server instance - no session state is maintained.
|
|
8
|
+
* This is ideal for stateless API integrations and serverless environments.
|
|
9
|
+
*
|
|
10
|
+
* @param createMcpServer Function to create an MCP server
|
|
11
|
+
* @param options Configuration options including optional schema validation and Express app
|
|
12
|
+
* @returns Express app
|
|
13
|
+
*/
|
|
14
|
+
export function createStatelessServer(createMcpServer, options) {
|
|
15
|
+
const app = options?.app ?? express();
|
|
16
|
+
app.use("/mcp", express.json());
|
|
17
|
+
// Handle POST requests for client-to-server communication
|
|
18
|
+
app.post("/mcp", async (req, res) => {
|
|
19
|
+
// In stateless mode, create a new instance of transport and server for each request
|
|
20
|
+
// to ensure complete isolation. A single instance would cause request ID collisions
|
|
21
|
+
// when multiple clients connect concurrently.
|
|
22
|
+
try {
|
|
23
|
+
// Validate config for all requests in stateless mode
|
|
24
|
+
const configResult = parseAndValidateConfig(req, options?.schema);
|
|
25
|
+
if (!configResult.ok) {
|
|
26
|
+
const status = configResult.error.status || 400;
|
|
27
|
+
res.status(status).json(configResult.error);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const config = configResult.value;
|
|
31
|
+
// Create a fresh server instance for each request
|
|
32
|
+
const server = createMcpServer({
|
|
33
|
+
config,
|
|
34
|
+
});
|
|
35
|
+
// Create a new transport for this request (no session management)
|
|
36
|
+
const transport = new StreamableHTTPServerTransport({
|
|
37
|
+
sessionIdGenerator: undefined,
|
|
38
|
+
});
|
|
39
|
+
// Clean up resources when request closes
|
|
40
|
+
res.on("close", () => {
|
|
41
|
+
transport.close();
|
|
42
|
+
server.close();
|
|
43
|
+
});
|
|
44
|
+
// Connect to the MCP server
|
|
45
|
+
await server.connect(transport);
|
|
46
|
+
// Handle the request directly
|
|
47
|
+
await transport.handleRequest(req, res, req.body);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error("Error handling MCP request:", error);
|
|
51
|
+
if (!res.headersSent) {
|
|
52
|
+
res.status(500).json({
|
|
53
|
+
jsonrpc: "2.0",
|
|
54
|
+
error: {
|
|
55
|
+
code: -32603,
|
|
56
|
+
message: "Internal server error",
|
|
57
|
+
},
|
|
58
|
+
id: null,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
// SSE notifications not supported in stateless mode
|
|
64
|
+
app.get("/mcp", async (_req, res) => {
|
|
65
|
+
res.status(405).json({
|
|
66
|
+
jsonrpc: "2.0",
|
|
67
|
+
error: {
|
|
68
|
+
code: -32000,
|
|
69
|
+
message: "Method not allowed.",
|
|
70
|
+
},
|
|
71
|
+
id: null,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
// Session termination not needed in stateless mode
|
|
75
|
+
app.delete("/mcp", async (_req, res) => {
|
|
76
|
+
res.status(405).json({
|
|
77
|
+
jsonrpc: "2.0",
|
|
78
|
+
error: {
|
|
79
|
+
code: -32000,
|
|
80
|
+
message: "Method not allowed.",
|
|
81
|
+
},
|
|
82
|
+
id: null,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
// Add .well-known/mcp-config endpoint for configuration discovery
|
|
86
|
+
app.get("/.well-known/mcp-config", (req, res) => {
|
|
87
|
+
// Set proper content type for JSON Schema
|
|
88
|
+
res.set("Content-Type", "application/schema+json; charset=utf-8");
|
|
89
|
+
const baseSchema = options?.schema
|
|
90
|
+
? zodToJsonSchema(options.schema)
|
|
91
|
+
: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {},
|
|
94
|
+
required: [],
|
|
95
|
+
};
|
|
96
|
+
const configSchema = {
|
|
97
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
98
|
+
$id: `${req.protocol}://${req.get("host")}/.well-known/mcp-config`,
|
|
99
|
+
title: "MCP Session Configuration",
|
|
100
|
+
description: "Schema for the /mcp endpoint configuration",
|
|
101
|
+
"x-query-style": "dot+bracket",
|
|
102
|
+
...baseSchema,
|
|
103
|
+
};
|
|
104
|
+
res.json(configSchema);
|
|
105
|
+
});
|
|
106
|
+
return { app };
|
|
107
|
+
}
|
package/dist/shared/config.d.ts
CHANGED
|
@@ -9,28 +9,27 @@ export interface SmitheryUrlOptions {
|
|
|
9
9
|
* Creates a URL to connect to the Smithery MCP server.
|
|
10
10
|
* @param baseUrl The base URL of the Smithery server
|
|
11
11
|
* @param options Optional configuration object
|
|
12
|
-
* @returns A URL
|
|
12
|
+
* @returns A URL with config encoded using dot-notation query params (e.g. model.name=gpt-4&debug=true)
|
|
13
13
|
*/
|
|
14
14
|
export declare function createSmitheryUrl(baseUrl: string, options?: SmitheryUrlOptions): URL;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
16
|
+
* General-purpose parser for config from key-value entries (e.g., URLSearchParams.entries()).
|
|
17
|
+
* Reserved keys (api_key, profile, config) are ignored.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseConfigFromEntries(entries: Iterable<[string, unknown]>): Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* Parses the config from an Express request using dot-notation parameters.
|
|
22
|
+
* Reserved keys (api_key, profile, config) are ignored.
|
|
19
23
|
*/
|
|
20
24
|
export declare function parseExpressRequestConfig(req: ExpressRequest): Record<string, unknown>;
|
|
21
25
|
/**
|
|
22
26
|
* Parses and validates config from an Express request with optional Zod schema validation
|
|
23
|
-
* Supports
|
|
27
|
+
* Supports dot-notation config parameters (e.g., foo=bar, a.b=c)
|
|
24
28
|
* @param req The express request
|
|
25
29
|
* @param schema Optional Zod schema for validation
|
|
26
30
|
* @returns Result with either parsed data or error response
|
|
27
31
|
*/
|
|
28
32
|
export declare function parseAndValidateConfig<T = Record<string, unknown>>(req: ExpressRequest, schema?: z.ZodSchema<T>): import("okay-error").Err<{
|
|
29
|
-
title: string;
|
|
30
|
-
status: number;
|
|
31
|
-
detail: string;
|
|
32
|
-
instance: string;
|
|
33
|
-
}> | import("okay-error").Err<{
|
|
34
33
|
readonly title: "Invalid configuration parameters";
|
|
35
34
|
readonly status: 422;
|
|
36
35
|
readonly detail: "One or more config parameters are invalid.";
|
package/dist/shared/config.js
CHANGED
|
@@ -1,19 +1,54 @@
|
|
|
1
1
|
import _ from "lodash";
|
|
2
2
|
import { err, ok } from "okay-error";
|
|
3
3
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
4
|
+
function isPlainObject(value) {
|
|
5
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function appendConfigAsDotParams(url, config) {
|
|
8
|
+
function add(pathParts, value) {
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
for (let index = 0; index < value.length; index++) {
|
|
11
|
+
add([...pathParts, String(index)], value[index]);
|
|
12
|
+
}
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (isPlainObject(value)) {
|
|
16
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
17
|
+
add([...pathParts, key], nested);
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const key = pathParts.join(".");
|
|
22
|
+
let stringValue;
|
|
23
|
+
switch (typeof value) {
|
|
24
|
+
case "string":
|
|
25
|
+
stringValue = value;
|
|
26
|
+
break;
|
|
27
|
+
case "number":
|
|
28
|
+
case "boolean":
|
|
29
|
+
stringValue = String(value);
|
|
30
|
+
break;
|
|
31
|
+
default:
|
|
32
|
+
stringValue = JSON.stringify(value);
|
|
33
|
+
}
|
|
34
|
+
url.searchParams.set(key, stringValue);
|
|
35
|
+
}
|
|
36
|
+
if (isPlainObject(config)) {
|
|
37
|
+
for (const [key, value] of Object.entries(config)) {
|
|
38
|
+
add([key], value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
4
42
|
/**
|
|
5
43
|
* Creates a URL to connect to the Smithery MCP server.
|
|
6
44
|
* @param baseUrl The base URL of the Smithery server
|
|
7
45
|
* @param options Optional configuration object
|
|
8
|
-
* @returns A URL
|
|
46
|
+
* @returns A URL with config encoded using dot-notation query params (e.g. model.name=gpt-4&debug=true)
|
|
9
47
|
*/
|
|
10
48
|
export function createSmitheryUrl(baseUrl, options) {
|
|
11
49
|
const url = new URL(`${baseUrl}/mcp`);
|
|
12
50
|
if (options?.config) {
|
|
13
|
-
|
|
14
|
-
? btoa(JSON.stringify(options.config))
|
|
15
|
-
: Buffer.from(JSON.stringify(options.config)).toString("base64");
|
|
16
|
-
url.searchParams.set("config", param);
|
|
51
|
+
appendConfigAsDotParams(url, options.config);
|
|
17
52
|
}
|
|
18
53
|
if (options?.apiKey) {
|
|
19
54
|
url.searchParams.set("api_key", options.apiKey);
|
|
@@ -24,42 +59,48 @@ export function createSmitheryUrl(baseUrl, options) {
|
|
|
24
59
|
return url;
|
|
25
60
|
}
|
|
26
61
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
62
|
+
* General-purpose parser for config from key-value entries (e.g., URLSearchParams.entries()).
|
|
63
|
+
* Reserved keys (api_key, profile, config) are ignored.
|
|
64
|
+
*/
|
|
65
|
+
export function parseConfigFromEntries(entries) {
|
|
66
|
+
const parsed = {};
|
|
67
|
+
for (const [key, raw] of entries) {
|
|
68
|
+
if (key === "api_key" || key === "profile" || key === "config")
|
|
69
|
+
continue;
|
|
70
|
+
const rawValue = Array.isArray(raw) ? raw[0] : raw;
|
|
71
|
+
if (typeof rawValue !== "string")
|
|
72
|
+
continue;
|
|
73
|
+
let castValue = rawValue;
|
|
74
|
+
try {
|
|
75
|
+
castValue = JSON.parse(rawValue);
|
|
76
|
+
}
|
|
77
|
+
catch { }
|
|
78
|
+
_.set(parsed, key.split("."), castValue);
|
|
79
|
+
}
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Parses the config from an Express request using dot-notation parameters.
|
|
84
|
+
* Reserved keys (api_key, profile, config) are ignored.
|
|
30
85
|
*/
|
|
31
86
|
export function parseExpressRequestConfig(req) {
|
|
32
|
-
return
|
|
87
|
+
return parseConfigFromEntries(Object.entries(req.query));
|
|
33
88
|
}
|
|
34
89
|
/**
|
|
35
90
|
* Parses and validates config from an Express request with optional Zod schema validation
|
|
36
|
-
* Supports
|
|
91
|
+
* Supports dot-notation config parameters (e.g., foo=bar, a.b=c)
|
|
37
92
|
* @param req The express request
|
|
38
93
|
* @param schema Optional Zod schema for validation
|
|
39
94
|
* @returns Result with either parsed data or error response
|
|
40
95
|
*/
|
|
41
96
|
export function parseAndValidateConfig(req, schema) {
|
|
42
97
|
// Parse config from request parameters
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
if (req.query.config) {
|
|
46
|
-
try {
|
|
47
|
-
config = parseExpressRequestConfig(req);
|
|
48
|
-
}
|
|
49
|
-
catch (configError) {
|
|
50
|
-
return err({
|
|
51
|
-
title: "Invalid config parameter",
|
|
52
|
-
status: 400,
|
|
53
|
-
detail: "Failed to parse config parameter",
|
|
54
|
-
instance: req.originalUrl,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
// 2. Process dot-notation config parameters (foo=bar, a.b=c)
|
|
98
|
+
const config = {};
|
|
99
|
+
// Process dot-notation config parameters (foo=bar, a.b=c)
|
|
59
100
|
// This allows URL params like ?server.host=localhost&server.port=8080&debug=true
|
|
60
101
|
for (const [key, value] of Object.entries(req.query)) {
|
|
61
102
|
// Skip reserved parameters
|
|
62
|
-
if (key === "
|
|
103
|
+
if (key === "api_key" || key === "profile")
|
|
63
104
|
continue;
|
|
64
105
|
const pathParts = key.split(".");
|
|
65
106
|
// Handle array values from Express query parsing
|