@smithery/sdk 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,49 +1,6 @@
1
- # Smithery Typescript SDK
1
+ # Smithery TypeScript SDK
2
2
 
3
- The SDK provides files for you to easily setup Smithery-compatible MCP servers and clients.
3
+ The SDK provides the types and bundle manifest schemas for building and deploying MCP servers on Smithery.
4
4
 
5
- ## Installation
6
-
7
- ```bash
8
- npm install @smithery/sdk @modelcontextprotocol/sdk
9
- ```
10
-
11
- ## Usage
12
-
13
- ### Spawning a Server
14
-
15
- Here's a minimal example of how to use the SDK to spawn an MCP server.
16
-
17
- ```typescript
18
- import { createStatelessServer } from '@smithery/sdk/server/stateless.js'
19
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
20
-
21
- // Create your MCP server function
22
- function createMcpServer({ config }) {
23
- // Create and return a server instance
24
- // https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#core-concepts
25
- const mcpServer = new McpServer({
26
- name: "My App",
27
- version: "1.0.0"
28
- })
29
-
30
- // ...
31
-
32
- return mcpServer.server
33
- }
34
-
35
- // Create the stateless server using your MCP server function.
36
- createStatelessServer(createMcpServer)
37
- .app
38
- .listen(process.env.PORT || 3000)
39
- ```
40
-
41
- This example:
42
- 1. Creates a stateless server that handles MCP requests
43
- 2. Defines a function to create MCP server instances for each session
44
- 3. Starts the Express server on the specified port. You must listen on the PORT env var if provided for the deployment to work on Smithery.
45
-
46
- #### Stateful Server
47
- Most API integrations are stateless.
48
-
49
- However, if your MCP server needs to persist state between calls (i.e., remembering previous interactions in a single chat conversation), you can use the `createStatefulServer` function instead.
5
+ For getting started, see the official documentation:
6
+ [https://smithery.ai/docs/getting_started/quickstart_build_typescript](https://smithery.ai/docs/getting_started/quickstart_build_typescript)
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Config - loaded from smithery.config.ts
3
+ */
4
+ export interface Config {
5
+ build?: {
6
+ /**
7
+ * Path to the server's entry point.
8
+ * Default: detected from package.json "module" or "main"
9
+ */
10
+ entry?: string;
11
+ /**
12
+ * Optional esbuild overrides for bundling
13
+ */
14
+ esbuild?: Record<string, unknown>;
15
+ };
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export * from "./manifest.js";
2
+ export * from "./limits.js";
3
+ export * from "./config.js";
@@ -0,0 +1,3 @@
1
+ export * from "./manifest.js";
2
+ export * from "./limits.js";
3
+ export * from "./config.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Validate bundle size limits
3
+ */
4
+ export declare const BUNDLE_SIZE_LIMITS: {
5
+ module: number;
6
+ sourcemap: number;
7
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Validate bundle size limits
3
+ */
4
+ export const BUNDLE_SIZE_LIMITS = {
5
+ module: 5 * 1024 * 1024, // 5 MB
6
+ sourcemap: 10 * 1024 * 1024, // 10 MB
7
+ };
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ export declare const BundleManifestSchema: z.ZodObject<{
3
+ schemaVersion: z.ZodLiteral<"smithery.bundle.v1">;
4
+ runtimeApiVersion: z.ZodLiteral<"smithery.isolate.v1">;
5
+ entry: z.ZodObject<{
6
+ type: z.ZodLiteral<"esm">;
7
+ export: z.ZodDefault<z.ZodString>;
8
+ }, z.core.$strip>;
9
+ stateful: z.ZodOptional<z.ZodBoolean>;
10
+ configSchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
11
+ capabilities: z.ZodOptional<z.ZodObject<{
12
+ tools: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
13
+ resources: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
14
+ prompts: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
15
+ }, z.core.$strip>>;
16
+ build: z.ZodOptional<z.ZodObject<{
17
+ repo: z.ZodOptional<z.ZodString>;
18
+ commit: z.ZodOptional<z.ZodString>;
19
+ branch: z.ZodOptional<z.ZodString>;
20
+ builtAt: z.ZodOptional<z.ZodString>;
21
+ }, z.core.$strip>>;
22
+ }, z.core.$strip>;
23
+ export type BundleManifest = z.infer<typeof BundleManifestSchema>;
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ export const BundleManifestSchema = z.object({
3
+ schemaVersion: z.literal("smithery.bundle.v1"),
4
+ runtimeApiVersion: z.literal("smithery.isolate.v1"),
5
+ entry: z.object({
6
+ type: z.literal("esm"),
7
+ export: z.string().default("default"),
8
+ }),
9
+ stateful: z.boolean().optional(),
10
+ configSchema: z.record(z.string(), z.unknown()).optional(),
11
+ capabilities: z
12
+ .object({
13
+ tools: z.array(z.record(z.string(), z.unknown())).optional(),
14
+ resources: z.array(z.record(z.string(), z.unknown())).optional(),
15
+ prompts: z.array(z.record(z.string(), z.unknown())).optional(),
16
+ })
17
+ .optional(),
18
+ build: z
19
+ .object({
20
+ repo: z.string().optional(),
21
+ commit: z.string().optional(),
22
+ branch: z.string().optional(),
23
+ builtAt: z.string().optional(),
24
+ })
25
+ .optional(),
26
+ });
package/dist/index.d.ts CHANGED
@@ -1,8 +1,2 @@
1
- export * from "./shared/config.js";
2
- export * from "./shared/patch.js";
3
- export { createStatefulServer, type StatefulServerOptions, } from "./server/stateful.js";
4
- export { createStatelessServer, type StatelessServerOptions, } from "./server/stateless.js";
5
- export * from "./server/logger.js";
6
- export * from "./server/session.js";
7
- export * from "./server/auth/identity.js";
8
- export * from "./server/auth/oauth.js";
1
+ export * from "./types/index.js";
2
+ export * from "./bundle/index.js";
package/dist/index.js CHANGED
@@ -1,13 +1,5 @@
1
1
  // Smithery SDK – Main exports
2
- // Use subpath imports for tree-shaking: @smithery/sdk/server, /helpers
3
- // === Shared Utilities ===
4
- export * from "./shared/config.js";
5
- export * from "./shared/patch.js";
6
- // === Server Primitives ===
7
- // Stateful/stateless server patterns, session management, auth
8
- export { createStatefulServer, } from "./server/stateful.js";
9
- export { createStatelessServer, } from "./server/stateless.js";
10
- export * from "./server/logger.js";
11
- export * from "./server/session.js";
12
- export * from "./server/auth/identity.js";
13
- export * from "./server/auth/oauth.js";
2
+ // Types for MCP server authors
3
+ export * from "./types/index.js";
4
+ // Bundle manifest schema (for CLI/registry)
5
+ export * from "./bundle/index.js";
@@ -0,0 +1,38 @@
1
+ import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import type { z } from "zod";
3
+ export type Session = {
4
+ id: string;
5
+ get: <T = unknown>(key: string) => Promise<T | undefined>;
6
+ set: (key: string, value: unknown) => Promise<void>;
7
+ delete: (key: string) => Promise<void>;
8
+ };
9
+ export type StatelessServerContext<TConfig = unknown> = {
10
+ config: TConfig;
11
+ env: Record<string, string | undefined>;
12
+ };
13
+ export type StatefulServerContext<TConfig = unknown> = {
14
+ config: TConfig;
15
+ session: Session;
16
+ env: Record<string, string | undefined>;
17
+ };
18
+ export type ServerContext<TConfig = unknown> = StatelessServerContext<TConfig> | StatefulServerContext<TConfig>;
19
+ export type SandboxServerContext = {
20
+ session: Session;
21
+ };
22
+ export type CreateServerFn<TConfig = unknown> = (context: ServerContext<TConfig>) => Server | Promise<Server>;
23
+ export type CreateSandboxServerFn = (context: SandboxServerContext) => Server | Promise<Server>;
24
+ /**
25
+ * ServerModule - expected exports from an MCP server entry point
26
+ */
27
+ export interface ServerModule<TConfig = unknown> {
28
+ default: CreateServerFn<TConfig>;
29
+ configSchema?: z.ZodSchema<TConfig>;
30
+ createSandboxServer?: CreateSandboxServerFn;
31
+ /**
32
+ * Whether the server is stateful.
33
+ * Stateful servers maintain state between calls within a session.
34
+ * Stateless servers are fresh for each request.
35
+ * @default false
36
+ */
37
+ stateful?: boolean;
38
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithery/sdk",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "SDK to develop with Smithery",
5
5
  "type": "module",
6
6
  "repository": {
@@ -10,40 +10,32 @@
10
10
  "main": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts",
12
12
  "exports": {
13
- ".": "./dist/index.js"
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./bundle": {
18
+ "types": "./dist/bundle/index.d.ts",
19
+ "default": "./dist/bundle/index.js"
20
+ }
14
21
  },
15
22
  "files": [
16
23
  "dist"
17
24
  ],
18
25
  "scripts": {
19
- "build": "tsc",
20
- "build:all": "pnpm -r --filter './*' build",
21
- "watch": "tsc --watch",
26
+ "build": "rm -rf dist && tsc",
22
27
  "check": "pnpm exec biome check --write --unsafe",
23
28
  "prepare": "pnpm run build"
24
29
  },
25
30
  "packageManager": "pnpm@9.0.0",
26
31
  "license": "MIT",
27
- "dependencies": {
28
- "@modelcontextprotocol/sdk": "^1.25.1",
29
- "chalk": "^5.6.2",
30
- "express": "^5.1.0",
31
- "jose": "^6.1.0",
32
- "lodash": "^4.17.21",
33
- "okay-error": "^1.0.3"
34
- },
35
32
  "peerDependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.25.1",
36
34
  "zod": "^4"
37
35
  },
38
36
  "devDependencies": {
39
37
  "@biomejs/biome": "2.2.6",
40
- "@types/express": "^5.0.1",
41
- "@types/json-schema": "^7.0.15",
42
- "@types/lodash": "^4.17.17",
43
38
  "@types/node": "^20.0.0",
44
- "@types/uuid": "^9.0.7",
45
- "dotenv": "^16.4.7",
46
- "tsx": "^4.19.2",
47
39
  "typescript": "^5.0.0",
48
40
  "zod": "^4"
49
41
  }
@@ -1,18 +0,0 @@
1
- import type { Application, Request, Router } from "express";
2
- import { type JWTPayload } from "jose";
3
- import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
4
- export type IdentityJwtClaims = JWTPayload & Record<string, unknown>;
5
- export interface IdentityHandler {
6
- /** Base path to mount metadata and token endpoints. Default: "/" */
7
- basePath?: string;
8
- /** Expected JWT issuer. Default: "https://server.smithery.ai" */
9
- issuer?: string;
10
- /** JWKS URL for issuer. Default: "https://server.smithery.ai/.well-known/jwks.json" */
11
- jwksUrl?: string;
12
- /** Optional explicit token path. Overrides basePath+"token". */
13
- tokenPath?: string;
14
- /** Handle a JWT grant provided by an external identity provider (i.e., Smithery) and mint access tokens */
15
- handleJwtGrant: (claims: IdentityJwtClaims, req: Request) => Promise<OAuthTokens | null>;
16
- }
17
- export declare function createIdentityTokenRouter(options: IdentityHandler): Router;
18
- export declare function mountIdentity(app: Application, options: IdentityHandler): void;
@@ -1,55 +0,0 @@
1
- import express from "express";
2
- import { createRemoteJWKSet, jwtVerify } from "jose";
3
- function normalizeBasePath(basePath) {
4
- const value = basePath ?? "/";
5
- return value.endsWith("/") ? value : `${value}/`;
6
- }
7
- export function createIdentityTokenRouter(options) {
8
- const basePath = normalizeBasePath(options.basePath);
9
- const issuer = options.issuer ?? "https://server.smithery.ai";
10
- const jwksUrl = new URL(options.jwksUrl ?? "https://server.smithery.ai/.well-known/jwks.json");
11
- const tokenPath = typeof options.tokenPath === "string" && options.tokenPath.length > 0
12
- ? options.tokenPath
13
- : `${basePath}token`;
14
- // Create JWKS resolver once; jose caches keys internally
15
- const JWKS = createRemoteJWKSet(jwksUrl);
16
- const tokenRouter = express.Router();
17
- // urlencoded parser required for OAuth token requests
18
- tokenRouter.use(express.urlencoded({ extended: false }));
19
- tokenRouter.post(tokenPath, async (req, res, next) => {
20
- try {
21
- const grantType = typeof req.body?.grant_type === "string"
22
- ? req.body.grant_type
23
- : undefined;
24
- if (grantType !== "urn:ietf:params:oauth:grant-type:jwt-bearer")
25
- return next();
26
- const assertion = typeof req.body?.assertion === "string" ? req.body.assertion : undefined;
27
- if (!assertion) {
28
- res.status(400).json({
29
- error: "invalid_request",
30
- error_description: "Missing assertion",
31
- });
32
- return;
33
- }
34
- const host = req.get("host") ?? "localhost";
35
- const audience = `https://${host}${tokenPath}`;
36
- const { payload } = await jwtVerify(assertion, JWKS, {
37
- issuer,
38
- audience,
39
- algorithms: ["RS256"],
40
- });
41
- const result = await options.handleJwtGrant(payload, req);
42
- if (!result)
43
- return next();
44
- res.json(result);
45
- }
46
- catch (error) {
47
- console.error(error);
48
- res.status(400).json({ error: "invalid_grant" });
49
- }
50
- });
51
- return tokenRouter;
52
- }
53
- export function mountIdentity(app, options) {
54
- app.use(createIdentityTokenRouter(options));
55
- }
@@ -1,21 +0,0 @@
1
- import type { OAuthServerProvider, OAuthTokenVerifier } from "@modelcontextprotocol/sdk/server/auth/provider.js";
2
- import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
3
- import type { Application, Response } from "express";
4
- import { type IdentityHandler } from "./identity.js";
5
- export interface TokenVerifier extends OAuthTokenVerifier {
6
- verifyAccessToken: (token: string) => Promise<AuthInfo>;
7
- requiredScopes?: string[];
8
- resourceMetadataUrl?: string;
9
- }
10
- type ProviderVerifier = OAuthServerProvider & TokenVerifier;
11
- export interface OAuthProvider extends ProviderVerifier {
12
- basePath?: string;
13
- callbackPath?: string;
14
- handleOAuthCallback?: (code: string, state: string | undefined, res: Response) => Promise<URL>;
15
- }
16
- export interface OAuthMountOptions {
17
- provider?: OAuthProvider | TokenVerifier;
18
- identity?: IdentityHandler;
19
- }
20
- export declare function mountOAuth(app: Application, opts: OAuthMountOptions): void;
21
- export {};
@@ -1,155 +0,0 @@
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
- }
@@ -1,5 +0,0 @@
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";
@@ -1,5 +0,0 @@
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";
@@ -1,19 +0,0 @@
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 {};
@@ -1,76 +0,0 @@
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
- }
@@ -1,17 +0,0 @@
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>;
@@ -1,36 +0,0 @@
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
- };
@@ -1,48 +0,0 @@
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 { McpServer } from "@modelcontextprotocol/sdk/server/mcp.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>) => McpServer["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
- };
@@ -1,170 +0,0 @@
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 * as zod from "zod";
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
- transport = sessionStore.get(sessionId);
35
- }
36
- else if (!sessionId && isInitializeRequest(req.body)) {
37
- // New initialization request
38
- const newSessionId = randomUUID();
39
- transport = new StreamableHTTPServerTransport({
40
- sessionIdGenerator: () => newSessionId,
41
- onsessioninitialized: sessionId => {
42
- // Store the transport by session ID
43
- sessionStore.set(sessionId, transport);
44
- },
45
- });
46
- // Clean up transport when closed
47
- transport.onclose = () => {
48
- if (transport.sessionId) {
49
- sessionStore.delete?.(transport.sessionId);
50
- }
51
- };
52
- // New session - validate config
53
- const configResult = parseAndValidateConfig(req, options?.schema);
54
- if (!configResult.ok) {
55
- const status = configResult.error.status || 400;
56
- logger.error({ error: configResult.error, sessionId: newSessionId }, "Config validation failed");
57
- res.status(status).json(configResult.error);
58
- return;
59
- }
60
- const config = configResult.value;
61
- try {
62
- logger.info({ sessionId: newSessionId }, "Creating new session");
63
- const server = createMcpServer({
64
- sessionId: newSessionId,
65
- config: config,
66
- auth: req.auth,
67
- logger,
68
- });
69
- // Connect to the MCP server
70
- await server.connect(transport);
71
- }
72
- catch (error) {
73
- logger.error({ error, sessionId: newSessionId }, "Error initializing server");
74
- res.status(500).json({
75
- jsonrpc: "2.0",
76
- error: {
77
- code: -32603,
78
- message: "Error initializing server.",
79
- },
80
- id: null,
81
- });
82
- return;
83
- }
84
- }
85
- else {
86
- // Invalid request
87
- logger.warn({ sessionId }, "Session not found or expired");
88
- res.status(400).json({
89
- jsonrpc: "2.0",
90
- error: {
91
- code: -32000,
92
- message: "Session not found or expired",
93
- },
94
- id: null,
95
- });
96
- return;
97
- }
98
- // Handle the request
99
- await transport.handleRequest(req, res, req.body);
100
- // Log successful response
101
- logger.debug({
102
- method: req.body.method,
103
- id: req.body.id,
104
- sessionId: req.headers["mcp-session-id"],
105
- }, "MCP Response sent");
106
- });
107
- // Add .well-known/mcp-config endpoint for configuration discovery
108
- app.get("/.well-known/mcp-config", (req, res) => {
109
- // Set proper content type for JSON Schema
110
- res.set("Content-Type", "application/schema+json; charset=utf-8");
111
- // Create schema with metadata using Zod's native .meta()
112
- const schema = (options?.schema ?? zod.object({})).meta({
113
- title: "MCP Session Configuration",
114
- description: "Schema for the /mcp endpoint configuration",
115
- "x-query-style": "dot+bracket",
116
- });
117
- const configSchema = {
118
- ...zod.toJSONSchema(schema, { target: "draft-2020-12" }),
119
- // $id is dynamic based on request, so we add it manually
120
- $id: `${req.protocol}://${req.get("host")}/.well-known/mcp-config`,
121
- };
122
- res.json(configSchema);
123
- });
124
- // Handle GET requests for server-to-client notifications via SSE
125
- app.get("/mcp", async (req, res) => {
126
- const sessionId = req.headers["mcp-session-id"];
127
- if (!sessionId || !sessionStore.get(sessionId)) {
128
- res.status(400).send("Invalid or expired session ID");
129
- return;
130
- }
131
- const transport = sessionStore.get(sessionId);
132
- await transport.handleRequest(req, res);
133
- });
134
- // Handle DELETE requests for session termination
135
- // https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management
136
- app.delete("/mcp", async (req, res) => {
137
- const sessionId = req.headers["mcp-session-id"];
138
- if (!sessionId) {
139
- logger.warn("Session termination request missing session ID");
140
- res.status(400).json({
141
- jsonrpc: "2.0",
142
- error: {
143
- code: -32600,
144
- message: "Missing mcp-session-id header",
145
- },
146
- id: null,
147
- });
148
- return;
149
- }
150
- const transport = sessionStore.get(sessionId);
151
- if (!transport) {
152
- logger.warn({ sessionId }, "Session termination failed - not found");
153
- res.status(404).json({
154
- jsonrpc: "2.0",
155
- error: {
156
- code: -32000,
157
- message: "Session not found or expired",
158
- },
159
- id: null,
160
- });
161
- return;
162
- }
163
- // Close the transport
164
- transport.close?.();
165
- logger.info({ sessionId }, "Session terminated");
166
- // Acknowledge session termination with 204 No Content
167
- res.status(204).end();
168
- });
169
- return { app };
170
- }
@@ -1,46 +0,0 @@
1
- import express from "express";
2
- import type { z } from "zod";
3
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.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>) => McpServer["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
- };
@@ -1,119 +0,0 @@
1
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2
- import express from "express";
3
- import * as zod from "zod";
4
- import { parseAndValidateConfig } from "../shared/config.js";
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
- // Create schema with metadata using Zod's native .meta()
106
- const schema = (options?.schema ?? zod.object({})).meta({
107
- title: "MCP Session Configuration",
108
- description: "Schema for the /mcp endpoint configuration",
109
- "x-query-style": "dot+bracket",
110
- });
111
- const configSchema = {
112
- ...zod.toJSONSchema(schema, { target: "draft-2020-12" }),
113
- // $id is dynamic based on request, so we add it manually
114
- $id: `${req.protocol}://${req.get("host")}/.well-known/mcp-config`,
115
- };
116
- res.json(configSchema);
117
- });
118
- return { app };
119
- }
@@ -1,37 +0,0 @@
1
- import type { Request as ExpressRequest } from "express";
2
- import * as 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: z.core.ZodStandardJSONSchemaPayload<z.ZodType<T, unknown, z.core.$ZodTypeInternals<T, unknown>>>;
29
- readonly errors: {
30
- param: string;
31
- pointer: string;
32
- reason: string;
33
- received: unknown;
34
- }[];
35
- readonly help: "Pass config as URL query params. Example: /mcp?param1=value1&param2=value2";
36
- }> | import("okay-error").Ok<T>;
37
- export declare function parseConfigFromQuery(query: Iterable<[string, unknown]>): Record<string, unknown>;
@@ -1,134 +0,0 @@
1
- import _ from "lodash";
2
- import { err, ok } from "okay-error";
3
- import * as z from "zod";
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 = z.toJSONSchema(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
- const keyStr = String(key);
80
- if (received && typeof received === "object" && keyStr in received) {
81
- received = received[keyStr];
82
- }
83
- else {
84
- received = undefined;
85
- break;
86
- }
87
- }
88
- return {
89
- param: issue.path.join(".") || "root",
90
- pointer: `/${issue.path.join("/")}`,
91
- reason: issue.message,
92
- received,
93
- };
94
- });
95
- return err({
96
- title: "Invalid configuration parameters",
97
- status: 422,
98
- detail: "One or more config parameters are invalid.",
99
- instance: req.originalUrl,
100
- configSchema: jsonSchema,
101
- errors,
102
- help: "Pass config as URL query params. Example: /mcp?param1=value1&param2=value2",
103
- });
104
- }
105
- return ok(result.data);
106
- }
107
- return ok(config);
108
- }
109
- // Process dot-notation config parameters from query parameters (foo=bar, a.b=c)
110
- // This allows URL params like ?server.host=localhost&server.port=8080&debug=true
111
- export function parseConfigFromQuery(query) {
112
- const config = {};
113
- for (const [key, value] of query) {
114
- // Skip reserved parameters
115
- if (key === "api_key" || key === "profile")
116
- continue;
117
- const pathParts = key.split(".");
118
- // Handle array values from Express query parsing
119
- const rawValue = Array.isArray(value) ? value[0] : value;
120
- if (typeof rawValue !== "string")
121
- continue;
122
- // Try to parse value as JSON (for booleans, numbers, objects)
123
- let parsedValue = rawValue;
124
- try {
125
- parsedValue = JSON.parse(rawValue);
126
- }
127
- catch {
128
- // If parsing fails, use the raw string value
129
- }
130
- // Use lodash's set method to handle nested paths
131
- _.set(config, pathParts, parsedValue);
132
- }
133
- return config;
134
- }
@@ -1,12 +0,0 @@
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;
@@ -1,12 +0,0 @@
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
- }