@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.
@@ -0,0 +1,123 @@
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
+ import { createLogger } from "./logger.js";
6
+ /**
7
+ * Creates a stateless server for handling MCP requests.
8
+ * Each request creates a new server instance - no session state is maintained.
9
+ * This is ideal for stateless API integrations and serverless environments.
10
+ *
11
+ * @param createMcpServer Function to create an MCP server
12
+ * @param options Configuration options including optional schema validation and Express app
13
+ * @returns Express app
14
+ */
15
+ export function createStatelessServer(createMcpServer, options) {
16
+ const app = options?.app ?? express();
17
+ const logger = createLogger(options?.logLevel ?? "info");
18
+ app.use("/mcp", express.json());
19
+ // Handle POST requests for client-to-server communication
20
+ app.post("/mcp", async (req, res) => {
21
+ // In stateless mode, create a new instance of transport and server for each request
22
+ // to ensure complete isolation. A single instance would cause request ID collisions
23
+ // when multiple clients connect concurrently.
24
+ try {
25
+ // Log incoming MCP request
26
+ logger.debug({
27
+ method: req.body.method,
28
+ id: req.body.id,
29
+ params: req.body.params,
30
+ }, "MCP Request");
31
+ // Validate config for all requests in stateless mode
32
+ const configResult = parseAndValidateConfig(req, options?.schema);
33
+ if (!configResult.ok) {
34
+ const status = configResult.error.status || 400;
35
+ logger.error({ error: configResult.error }, "Config validation failed");
36
+ res.status(status).json(configResult.error);
37
+ return;
38
+ }
39
+ const config = configResult.value;
40
+ // Create a fresh server instance for each request
41
+ const server = createMcpServer({
42
+ config,
43
+ auth: req.auth,
44
+ logger,
45
+ });
46
+ // Create a new transport for this request (no session management)
47
+ const transport = new StreamableHTTPServerTransport({
48
+ sessionIdGenerator: undefined,
49
+ });
50
+ // Clean up resources when request closes
51
+ res.on("close", () => {
52
+ transport.close();
53
+ server.close();
54
+ });
55
+ // Connect to the MCP server
56
+ await server.connect(transport);
57
+ // Handle the request directly
58
+ await transport.handleRequest(req, res, req.body);
59
+ // Log successful response
60
+ logger.debug({
61
+ method: req.body.method,
62
+ id: req.body.id,
63
+ }, "MCP Response sent");
64
+ }
65
+ catch (error) {
66
+ logger.error({ error }, "Error handling MCP request");
67
+ if (!res.headersSent) {
68
+ res.status(500).json({
69
+ jsonrpc: "2.0",
70
+ error: {
71
+ code: -32603,
72
+ message: "Internal server error",
73
+ },
74
+ id: null,
75
+ });
76
+ }
77
+ }
78
+ });
79
+ // SSE notifications not supported in stateless mode
80
+ app.get("/mcp", async (_req, res) => {
81
+ res.status(405).json({
82
+ jsonrpc: "2.0",
83
+ error: {
84
+ code: -32000,
85
+ message: "Method not allowed.",
86
+ },
87
+ id: null,
88
+ });
89
+ });
90
+ // Session termination not needed in stateless mode
91
+ app.delete("/mcp", async (_req, res) => {
92
+ res.status(405).json({
93
+ jsonrpc: "2.0",
94
+ error: {
95
+ code: -32000,
96
+ message: "Method not allowed.",
97
+ },
98
+ id: null,
99
+ });
100
+ });
101
+ // Add .well-known/mcp-config endpoint for configuration discovery
102
+ app.get("/.well-known/mcp-config", (req, res) => {
103
+ // Set proper content type for JSON Schema
104
+ res.set("Content-Type", "application/schema+json; charset=utf-8");
105
+ const baseSchema = options?.schema
106
+ ? zodToJsonSchema(options.schema)
107
+ : {
108
+ type: "object",
109
+ properties: {},
110
+ required: [],
111
+ };
112
+ const configSchema = {
113
+ $schema: "https://json-schema.org/draft/2020-12/schema",
114
+ $id: `${req.protocol}://${req.get("host")}/.well-known/mcp-config`,
115
+ title: "MCP Session Configuration",
116
+ description: "Schema for the /mcp endpoint configuration",
117
+ "x-query-style": "dot+bracket",
118
+ ...baseSchema,
119
+ };
120
+ res.json(configSchema);
121
+ });
122
+ return { app };
123
+ }
@@ -0,0 +1,6 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export interface WidgetServerOptions {
3
+ name: string;
4
+ version: string;
5
+ }
6
+ export declare function createWidgetServer(options: WidgetServerOptions): McpServer;
@@ -0,0 +1,7 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export function createWidgetServer(options) {
3
+ return new McpServer({
4
+ name: options.name,
5
+ version: options.version,
6
+ });
7
+ }
@@ -0,0 +1,42 @@
1
+ import type { Request as ExpressRequest } from "express";
2
+ import type { z } from "zod";
3
+ export interface SmitheryUrlOptions {
4
+ apiKey?: string;
5
+ profile?: string;
6
+ config?: object;
7
+ }
8
+ export declare function appendConfigAsDotParams(url: URL, config: unknown): void;
9
+ /**
10
+ * Creates a URL to connect to the Smithery MCP server.
11
+ * @param baseUrl The base URL of the Smithery server
12
+ * @param options Optional configuration object
13
+ * @returns A URL with config encoded using dot-notation query params (e.g. model.name=gpt-4&debug=true)
14
+ */
15
+ export declare function createSmitheryUrl(baseUrl: string, options?: SmitheryUrlOptions): URL;
16
+ /**
17
+ * Parses and validates config from an Express request with optional Zod schema validation
18
+ * Supports dot-notation config parameters (e.g., foo=bar, a.b=c)
19
+ * @param req The express request
20
+ * @param schema Optional Zod schema for validation
21
+ * @returns Result with either parsed data or error response
22
+ */
23
+ export declare function parseAndValidateConfig<T = Record<string, unknown>>(req: ExpressRequest, schema?: z.ZodSchema<T>): import("okay-error").Err<{
24
+ readonly title: "Invalid configuration parameters";
25
+ readonly status: 422;
26
+ readonly detail: "One or more config parameters are invalid.";
27
+ readonly instance: string;
28
+ readonly configSchema: import("zod-to-json-schema").JsonSchema7Type & {
29
+ $schema?: string | undefined;
30
+ definitions?: {
31
+ [key: string]: import("zod-to-json-schema").JsonSchema7Type;
32
+ } | undefined;
33
+ };
34
+ readonly errors: {
35
+ param: string;
36
+ pointer: string;
37
+ reason: string;
38
+ received: unknown;
39
+ }[];
40
+ readonly help: "Pass config as URL query params. Example: /mcp?param1=value1&param2=value2";
41
+ }> | import("okay-error").Ok<T>;
42
+ export declare function parseConfigFromQuery(query: Iterable<[string, unknown]>): Record<string, unknown>;
@@ -0,0 +1,133 @@
1
+ import _ from "lodash";
2
+ import { err, ok } from "okay-error";
3
+ import { zodToJsonSchema } from "zod-to-json-schema";
4
+ function isPlainObject(value) {
5
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6
+ }
7
+ export 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
+ }
42
+ /**
43
+ * Creates a URL to connect to the Smithery MCP server.
44
+ * @param baseUrl The base URL of the Smithery server
45
+ * @param options Optional configuration object
46
+ * @returns A URL with config encoded using dot-notation query params (e.g. model.name=gpt-4&debug=true)
47
+ */
48
+ export function createSmitheryUrl(baseUrl, options) {
49
+ const url = new URL(`${baseUrl}/mcp`);
50
+ if (options?.config) {
51
+ appendConfigAsDotParams(url, options.config);
52
+ }
53
+ if (options?.apiKey) {
54
+ url.searchParams.set("api_key", options.apiKey);
55
+ }
56
+ if (options?.profile) {
57
+ url.searchParams.set("profile", options.profile);
58
+ }
59
+ return url;
60
+ }
61
+ /**
62
+ * Parses and validates config from an Express request with optional Zod schema validation
63
+ * Supports dot-notation config parameters (e.g., foo=bar, a.b=c)
64
+ * @param req The express request
65
+ * @param schema Optional Zod schema for validation
66
+ * @returns Result with either parsed data or error response
67
+ */
68
+ export function parseAndValidateConfig(req, schema) {
69
+ const config = parseConfigFromQuery(Object.entries(req.query));
70
+ // Validate config against schema if provided
71
+ if (schema) {
72
+ const result = schema.safeParse(config);
73
+ if (!result.success) {
74
+ const jsonSchema = zodToJsonSchema(schema);
75
+ const errors = result.error.issues.map(issue => {
76
+ // Safely traverse the config object to get the received value
77
+ let received = config;
78
+ for (const key of issue.path) {
79
+ if (received && typeof received === "object" && key in received) {
80
+ received = received[key];
81
+ }
82
+ else {
83
+ received = undefined;
84
+ break;
85
+ }
86
+ }
87
+ return {
88
+ param: issue.path.join(".") || "root",
89
+ pointer: `/${issue.path.join("/")}`,
90
+ reason: issue.message,
91
+ received,
92
+ };
93
+ });
94
+ return err({
95
+ title: "Invalid configuration parameters",
96
+ status: 422,
97
+ detail: "One or more config parameters are invalid.",
98
+ instance: req.originalUrl,
99
+ configSchema: jsonSchema,
100
+ errors,
101
+ help: "Pass config as URL query params. Example: /mcp?param1=value1&param2=value2",
102
+ });
103
+ }
104
+ return ok(result.data);
105
+ }
106
+ return ok(config);
107
+ }
108
+ // Process dot-notation config parameters from query parameters (foo=bar, a.b=c)
109
+ // This allows URL params like ?server.host=localhost&server.port=8080&debug=true
110
+ export function parseConfigFromQuery(query) {
111
+ const config = {};
112
+ for (const [key, value] of query) {
113
+ // Skip reserved parameters
114
+ if (key === "api_key" || key === "profile")
115
+ continue;
116
+ const pathParts = key.split(".");
117
+ // Handle array values from Express query parsing
118
+ const rawValue = Array.isArray(value) ? value[0] : value;
119
+ if (typeof rawValue !== "string")
120
+ continue;
121
+ // Try to parse value as JSON (for booleans, numbers, objects)
122
+ let parsedValue = rawValue;
123
+ try {
124
+ parsedValue = JSON.parse(rawValue);
125
+ }
126
+ catch {
127
+ // If parsing fails, use the raw string value
128
+ }
129
+ // Use lodash's set method to handle nested paths
130
+ _.set(config, pathParts, parsedValue);
131
+ }
132
+ return config;
133
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Patches a function on an object
3
+ * @param obj
4
+ * @param key
5
+ * @param patcher
6
+ */
7
+ export declare function patch<T extends {
8
+ [P in K]: (...args: any[]) => any;
9
+ }, K extends keyof T & string>(obj: T, key: K, patcher: (fn: T[K]) => T[K]): void;
10
+ export declare function patch<T extends {
11
+ [P in K]?: (...args: any[]) => any;
12
+ }, K extends keyof T & string>(obj: T, key: K, patcher: (fn?: T[K]) => T[K]): void;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Patches a function on an object
3
+ * @param obj
4
+ * @param key
5
+ * @param patcher
6
+ */
7
+ // Unified implementation (not type-checked by callers)
8
+ export function patch(obj, key, patcher) {
9
+ // If the property is actually a function, bind it; otherwise undefined
10
+ const original = typeof obj[key] === "function" ? obj[key].bind(obj) : undefined;
11
+ obj[key] = patcher(original);
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithery/sdk",
3
- "version": "1.7.2",
3
+ "version": "1.7.3",
4
4
  "description": "SDK to develop with Smithery",
5
5
  "type": "module",
6
6
  "repository": {
@@ -39,7 +39,8 @@
39
39
  "build": "tsc",
40
40
  "build:all": "npm run build -ws --include-workspace-root",
41
41
  "watch": "tsc --watch",
42
- "check": "npx @biomejs/biome check --write --unsafe"
42
+ "check": "npx @biomejs/biome check --write --unsafe",
43
+ "prepare": "npm run build"
43
44
  },
44
45
  "packageManager": "npm@11.4.1",
45
46
  "license": "MIT",