@smithery/sdk
Advanced tools
| /** | ||
| * Config - loaded from smithery.config.ts | ||
| */ | ||
| export interface Config { | ||
| build?: { | ||
| /** | ||
| * Path to the server's entry point. | ||
| * Default: detected from package.json "module" or "main" | ||
| */ | ||
| entry?: string; | ||
| /** | ||
| * Optional esbuild overrides for bundling | ||
| */ | ||
| esbuild?: Record<string, unknown>; | ||
| }; | ||
| } |
| export {}; |
| export * from "./manifest.js"; | ||
| export * from "./limits.js"; | ||
| export * from "./config.js"; |
| export * from "./manifest.js"; | ||
| export * from "./limits.js"; | ||
| export * from "./config.js"; |
| /** | ||
| * Validate bundle size limits | ||
| */ | ||
| export declare const BUNDLE_SIZE_LIMITS: { | ||
| module: number; | ||
| sourcemap: number; | ||
| }; |
| /** | ||
| * Validate bundle size limits | ||
| */ | ||
| export const BUNDLE_SIZE_LIMITS = { | ||
| module: 5 * 1024 * 1024, // 5 MB | ||
| sourcemap: 10 * 1024 * 1024, // 10 MB | ||
| }; |
| import { z } from "zod"; | ||
| export declare const BundleManifestSchema: z.ZodObject<{ | ||
| schemaVersion: z.ZodLiteral<"smithery.bundle.v1">; | ||
| runtimeApiVersion: z.ZodLiteral<"smithery.isolate.v1">; | ||
| entry: z.ZodObject<{ | ||
| type: z.ZodLiteral<"esm">; | ||
| export: z.ZodDefault<z.ZodString>; | ||
| }, z.core.$strip>; | ||
| stateful: z.ZodOptional<z.ZodBoolean>; | ||
| configSchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>; | ||
| capabilities: z.ZodOptional<z.ZodObject<{ | ||
| tools: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>; | ||
| resources: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>; | ||
| prompts: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>; | ||
| }, z.core.$strip>>; | ||
| build: z.ZodOptional<z.ZodObject<{ | ||
| repo: z.ZodOptional<z.ZodString>; | ||
| commit: z.ZodOptional<z.ZodString>; | ||
| branch: z.ZodOptional<z.ZodString>; | ||
| builtAt: z.ZodOptional<z.ZodString>; | ||
| }, z.core.$strip>>; | ||
| }, z.core.$strip>; | ||
| export type BundleManifest = z.infer<typeof BundleManifestSchema>; |
| import { z } from "zod"; | ||
| export const BundleManifestSchema = z.object({ | ||
| schemaVersion: z.literal("smithery.bundle.v1"), | ||
| runtimeApiVersion: z.literal("smithery.isolate.v1"), | ||
| entry: z.object({ | ||
| type: z.literal("esm"), | ||
| export: z.string().default("default"), | ||
| }), | ||
| stateful: z.boolean().optional(), | ||
| configSchema: z.record(z.string(), z.unknown()).optional(), | ||
| capabilities: z | ||
| .object({ | ||
| tools: z.array(z.record(z.string(), z.unknown())).optional(), | ||
| resources: z.array(z.record(z.string(), z.unknown())).optional(), | ||
| prompts: z.array(z.record(z.string(), z.unknown())).optional(), | ||
| }) | ||
| .optional(), | ||
| build: z | ||
| .object({ | ||
| repo: z.string().optional(), | ||
| commit: z.string().optional(), | ||
| branch: z.string().optional(), | ||
| builtAt: z.string().optional(), | ||
| }) | ||
| .optional(), | ||
| }); |
| import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; | ||
| import type { z } from "zod"; | ||
| export type Session = { | ||
| id: string; | ||
| get: <T = unknown>(key: string) => Promise<T | undefined>; | ||
| set: (key: string, value: unknown) => Promise<void>; | ||
| delete: (key: string) => Promise<void>; | ||
| }; | ||
| export type StatelessServerContext<TConfig = unknown> = { | ||
| config: TConfig; | ||
| env: Record<string, string | undefined>; | ||
| }; | ||
| export type StatefulServerContext<TConfig = unknown> = { | ||
| config: TConfig; | ||
| session: Session; | ||
| env: Record<string, string | undefined>; | ||
| }; | ||
| export type ServerContext<TConfig = unknown> = StatelessServerContext<TConfig> | StatefulServerContext<TConfig>; | ||
| export type SandboxServerContext = { | ||
| session: Session; | ||
| }; | ||
| export type CreateServerFn<TConfig = unknown> = (context: ServerContext<TConfig>) => Server | Promise<Server>; | ||
| export type CreateSandboxServerFn = (context: SandboxServerContext) => Server | Promise<Server>; | ||
| /** | ||
| * ServerModule - expected exports from an MCP server entry point | ||
| */ | ||
| export interface ServerModule<TConfig = unknown> { | ||
| default: CreateServerFn<TConfig>; | ||
| configSchema?: z.ZodSchema<TConfig>; | ||
| createSandboxServer?: CreateSandboxServerFn; | ||
| /** | ||
| * Whether the server is stateful. | ||
| * Stateful servers maintain state between calls within a session. | ||
| * Stateless servers are fresh for each request. | ||
| * @default false | ||
| */ | ||
| stateful?: boolean; | ||
| } |
| export {}; |
+2
-8
@@ -1,8 +0,2 @@ | ||
| export * from "./shared/config.js"; | ||
| export * from "./shared/patch.js"; | ||
| export { createStatefulServer, type CreateServerFn, type StatefulServerOptions, } from "./server/stateful.js"; | ||
| export { createStatelessServer, type CreateStatelessServerFn, type StatelessServerOptions, } from "./server/stateless.js"; | ||
| export * from "./server/logger.js"; | ||
| export * from "./server/session.js"; | ||
| export * from "./server/auth/identity.js"; | ||
| export * from "./server/auth/oauth.js"; | ||
| export * from "./types/index.js"; | ||
| export * from "./bundle/index.js"; |
+4
-12
| // Smithery SDK – Main exports | ||
| // Use subpath imports for tree-shaking: @smithery/sdk/server, /helpers | ||
| // === Shared Utilities === | ||
| export * from "./shared/config.js"; | ||
| export * from "./shared/patch.js"; | ||
| // === Server Primitives === | ||
| // Stateful/stateless server patterns, session management, auth | ||
| export { createStatefulServer, } from "./server/stateful.js"; | ||
| export { createStatelessServer, } from "./server/stateless.js"; | ||
| export * from "./server/logger.js"; | ||
| export * from "./server/session.js"; | ||
| export * from "./server/auth/identity.js"; | ||
| export * from "./server/auth/oauth.js"; | ||
| // Types for MCP server authors | ||
| export * from "./types/index.js"; | ||
| // Bundle manifest schema (for CLI/registry) | ||
| export * from "./bundle/index.js"; |
+11
-19
| { | ||
| "name": "@smithery/sdk", | ||
| "version": "3.0.1", | ||
| "version": "4.0.0", | ||
| "description": "SDK to develop with Smithery", | ||
@@ -13,3 +13,10 @@ "type": "module", | ||
| "exports": { | ||
| ".": "./dist/index.js" | ||
| ".": { | ||
| "types": "./dist/index.d.ts", | ||
| "default": "./dist/index.js" | ||
| }, | ||
| "./bundle": { | ||
| "types": "./dist/bundle/index.d.ts", | ||
| "default": "./dist/bundle/index.js" | ||
| } | ||
| }, | ||
@@ -20,5 +27,3 @@ "files": [ | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "build:all": "pnpm -r --filter './*' build", | ||
| "watch": "tsc --watch", | ||
| "build": "rm -rf dist && tsc", | ||
| "check": "pnpm exec biome check --write --unsafe", | ||
@@ -29,11 +34,4 @@ "prepare": "pnpm run build" | ||
| "license": "MIT", | ||
| "dependencies": { | ||
| "peerDependencies": { | ||
| "@modelcontextprotocol/sdk": "^1.25.1", | ||
| "chalk": "^5.6.2", | ||
| "express": "^5.1.0", | ||
| "jose": "^6.1.0", | ||
| "lodash": "^4.17.21", | ||
| "okay-error": "^1.0.3" | ||
| }, | ||
| "peerDependencies": { | ||
| "zod": "^4" | ||
@@ -43,9 +41,3 @@ }, | ||
| "@biomejs/biome": "2.2.6", | ||
| "@types/express": "^5.0.1", | ||
| "@types/json-schema": "^7.0.15", | ||
| "@types/lodash": "^4.17.17", | ||
| "@types/node": "^20.0.0", | ||
| "@types/uuid": "^9.0.7", | ||
| "dotenv": "^16.4.7", | ||
| "tsx": "^4.19.2", | ||
| "typescript": "^5.0.0", | ||
@@ -52,0 +44,0 @@ "zod": "^4" |
+4
-47
@@ -1,49 +0,6 @@ | ||
| # Smithery Typescript SDK | ||
| # Smithery TypeScript SDK | ||
| The SDK provides files for you to easily setup Smithery-compatible MCP servers and clients. | ||
| The SDK provides the types and bundle manifest schemas for building and deploying MCP servers on Smithery. | ||
| ## Installation | ||
| ```bash | ||
| npm install @smithery/sdk @modelcontextprotocol/sdk | ||
| ``` | ||
| ## Usage | ||
| ### Spawning a Server | ||
| Here's a minimal example of how to use the SDK to spawn an MCP server. | ||
| ```typescript | ||
| import { createStatelessServer } from '@smithery/sdk/server/stateless.js' | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" | ||
| // Create your MCP server function | ||
| function createMcpServer({ config }) { | ||
| // Create and return a server instance | ||
| // https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#core-concepts | ||
| const mcpServer = new McpServer({ | ||
| name: "My App", | ||
| version: "1.0.0" | ||
| }) | ||
| // ... | ||
| return mcpServer.server | ||
| } | ||
| // Create the stateless server using your MCP server function. | ||
| createStatelessServer(createMcpServer) | ||
| .app | ||
| .listen(process.env.PORT || 3000) | ||
| ``` | ||
| This example: | ||
| 1. Creates a stateless server that handles MCP requests | ||
| 2. Defines a function to create MCP server instances for each session | ||
| 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. | ||
| #### Stateful Server | ||
| Most API integrations are stateless. | ||
| 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. | ||
| For getting started, see the official documentation: | ||
| [https://smithery.ai/docs/getting_started/quickstart_build_typescript](https://smithery.ai/docs/getting_started/quickstart_build_typescript) |
| import type { Application, Request, Router } from "express"; | ||
| import { type JWTPayload } from "jose"; | ||
| import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js"; | ||
| export type IdentityJwtClaims = JWTPayload & Record<string, unknown>; | ||
| export interface IdentityHandler { | ||
| /** Base path to mount metadata and token endpoints. Default: "/" */ | ||
| basePath?: string; | ||
| /** Expected JWT issuer. Default: "https://server.smithery.ai" */ | ||
| issuer?: string; | ||
| /** JWKS URL for issuer. Default: "https://server.smithery.ai/.well-known/jwks.json" */ | ||
| jwksUrl?: string; | ||
| /** Optional explicit token path. Overrides basePath+"token". */ | ||
| tokenPath?: string; | ||
| /** Handle a JWT grant provided by an external identity provider (i.e., Smithery) and mint access tokens */ | ||
| handleJwtGrant: (claims: IdentityJwtClaims, req: Request) => Promise<OAuthTokens | null>; | ||
| } | ||
| export declare function createIdentityTokenRouter(options: IdentityHandler): Router; | ||
| export declare function mountIdentity(app: Application, options: IdentityHandler): void; |
| import express from "express"; | ||
| import { createRemoteJWKSet, jwtVerify } from "jose"; | ||
| function normalizeBasePath(basePath) { | ||
| const value = basePath ?? "/"; | ||
| return value.endsWith("/") ? value : `${value}/`; | ||
| } | ||
| export function createIdentityTokenRouter(options) { | ||
| const basePath = normalizeBasePath(options.basePath); | ||
| const issuer = options.issuer ?? "https://server.smithery.ai"; | ||
| const jwksUrl = new URL(options.jwksUrl ?? "https://server.smithery.ai/.well-known/jwks.json"); | ||
| const tokenPath = typeof options.tokenPath === "string" && options.tokenPath.length > 0 | ||
| ? options.tokenPath | ||
| : `${basePath}token`; | ||
| // Create JWKS resolver once; jose caches keys internally | ||
| const JWKS = createRemoteJWKSet(jwksUrl); | ||
| const tokenRouter = express.Router(); | ||
| // urlencoded parser required for OAuth token requests | ||
| tokenRouter.use(express.urlencoded({ extended: false })); | ||
| tokenRouter.post(tokenPath, async (req, res, next) => { | ||
| try { | ||
| const grantType = typeof req.body?.grant_type === "string" | ||
| ? req.body.grant_type | ||
| : undefined; | ||
| if (grantType !== "urn:ietf:params:oauth:grant-type:jwt-bearer") | ||
| return next(); | ||
| const assertion = typeof req.body?.assertion === "string" ? req.body.assertion : undefined; | ||
| if (!assertion) { | ||
| res.status(400).json({ | ||
| error: "invalid_request", | ||
| error_description: "Missing assertion", | ||
| }); | ||
| return; | ||
| } | ||
| const host = req.get("host") ?? "localhost"; | ||
| const audience = `https://${host}${tokenPath}`; | ||
| const { payload } = await jwtVerify(assertion, JWKS, { | ||
| issuer, | ||
| audience, | ||
| algorithms: ["RS256"], | ||
| }); | ||
| const result = await options.handleJwtGrant(payload, req); | ||
| if (!result) | ||
| return next(); | ||
| res.json(result); | ||
| } | ||
| catch (error) { | ||
| console.error(error); | ||
| res.status(400).json({ error: "invalid_grant" }); | ||
| } | ||
| }); | ||
| return tokenRouter; | ||
| } | ||
| export function mountIdentity(app, options) { | ||
| app.use(createIdentityTokenRouter(options)); | ||
| } |
| import type { OAuthServerProvider, OAuthTokenVerifier } from "@modelcontextprotocol/sdk/server/auth/provider.js"; | ||
| import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; | ||
| import type { Application, Response } from "express"; | ||
| import { type IdentityHandler } from "./identity.js"; | ||
| export interface TokenVerifier extends OAuthTokenVerifier { | ||
| verifyAccessToken: (token: string) => Promise<AuthInfo>; | ||
| requiredScopes?: string[]; | ||
| resourceMetadataUrl?: string; | ||
| } | ||
| type ProviderVerifier = OAuthServerProvider & TokenVerifier; | ||
| export interface OAuthProvider extends ProviderVerifier { | ||
| basePath?: string; | ||
| callbackPath?: string; | ||
| handleOAuthCallback?: (code: string, state: string | undefined, res: Response) => Promise<URL>; | ||
| } | ||
| export interface OAuthMountOptions { | ||
| provider?: OAuthProvider | TokenVerifier; | ||
| identity?: IdentityHandler; | ||
| } | ||
| export declare function mountOAuth(app: Application, opts: OAuthMountOptions): void; | ||
| export {}; |
| import { authorizationHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/authorize.js"; | ||
| import { metadataHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/metadata.js"; | ||
| import { clientRegistrationHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/register.js"; | ||
| import { revocationHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/revoke.js"; | ||
| import { tokenHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/token.js"; | ||
| import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js"; | ||
| import { createOAuthMetadata, mcpAuthMetadataRouter, } from "@modelcontextprotocol/sdk/server/auth/router.js"; | ||
| import { mountIdentity } from "./identity.js"; | ||
| function isOAuthProvider(provider) { | ||
| return !!provider && "authorize" in provider; | ||
| } | ||
| export function mountOAuth(app, opts) { | ||
| // Determine base path once based on OAuth provider or identity | ||
| const provider = opts.provider; | ||
| const hasOAuth = isOAuthProvider(provider); | ||
| const rawBasePath = hasOAuth | ||
| ? (provider.basePath ?? "/") | ||
| : (opts.identity?.basePath ?? "/"); | ||
| const basePath = rawBasePath.endsWith("/") ? rawBasePath : `${rawBasePath}/`; | ||
| // Precompute endpoint pathnames from metadata | ||
| let authorizationPath; | ||
| let tokenPath; | ||
| let registrationPath; | ||
| let revocationPath; | ||
| if (isOAuthProvider(provider)) { | ||
| const placeholderIssuer = new URL("https://localhost"); | ||
| const placeholderBaseUrl = new URL(basePath, placeholderIssuer); | ||
| const localMetadata = createOAuthMetadata({ | ||
| provider, | ||
| issuerUrl: placeholderIssuer, | ||
| baseUrl: placeholderBaseUrl, | ||
| }); | ||
| authorizationPath = new URL(localMetadata.authorization_endpoint).pathname; | ||
| tokenPath = new URL(localMetadata.token_endpoint).pathname; | ||
| if (localMetadata.registration_endpoint) { | ||
| registrationPath = new URL(localMetadata.registration_endpoint).pathname; | ||
| } | ||
| if (localMetadata.revocation_endpoint) { | ||
| revocationPath = new URL(localMetadata.revocation_endpoint).pathname; | ||
| } | ||
| } | ||
| // Metadata endpoints | ||
| if (isOAuthProvider(provider)) { | ||
| // Mount a per-request adapter so issuer/baseUrl reflect Host/Proto | ||
| app.use((req, res, next) => { | ||
| if (!req.path.startsWith("/.well-known/")) | ||
| return next(); | ||
| const host = req.get("host") ?? "localhost"; | ||
| if (req.protocol !== "https") { | ||
| console.warn("Detected http but using https for issuer URL in OAuth metadata since it will fail otherwise."); | ||
| } | ||
| const issuerUrl = new URL(`https://${host}`); | ||
| const baseUrl = new URL(basePath, issuerUrl); | ||
| const oauthMetadata = createOAuthMetadata({ | ||
| provider, | ||
| issuerUrl, | ||
| baseUrl, | ||
| }); | ||
| if (opts.identity) { | ||
| oauthMetadata.grant_types_supported = Array.from(new Set([ | ||
| ...(oauthMetadata.grant_types_supported ?? []), | ||
| "urn:ietf:params:oauth:grant-type:jwt-bearer", | ||
| ])); | ||
| } | ||
| const resourceServerUrl = new URL("/mcp", issuerUrl); | ||
| const metadataRouter = mcpAuthMetadataRouter({ | ||
| oauthMetadata, | ||
| resourceServerUrl, | ||
| }); | ||
| return metadataRouter(req, res, next); | ||
| }); | ||
| } | ||
| else if (opts.identity) { | ||
| // Identity-only: explicitly mount protected resource metadata endpoint | ||
| app.use("/.well-known/oauth-protected-resource", (req, res, next) => { | ||
| const host = req.get("host") ?? "localhost"; | ||
| const issuerUrl = new URL(`https://${host}`); | ||
| const protectedResourceMetadata = { | ||
| resource: new URL("/mcp", issuerUrl).href, | ||
| authorization_servers: [issuerUrl.href], | ||
| }; | ||
| return metadataHandler(protectedResourceMetadata)(req, res, next); | ||
| }); | ||
| // Identity-only: also advertise minimal AS metadata for discovery per RFC 8414 | ||
| app.use("/.well-known/oauth-authorization-server", (req, res, next) => { | ||
| const host = req.get("host") ?? "localhost"; | ||
| const issuerUrl = new URL(`https://${host}`); | ||
| const oauthMetadata = { | ||
| issuer: issuerUrl.href, | ||
| token_endpoint: new URL(`${basePath}token`, issuerUrl).href, | ||
| grant_types_supported: ["urn:ietf:params:oauth:grant-type:jwt-bearer"], | ||
| }; | ||
| return metadataHandler(oauthMetadata)(req, res, next); | ||
| }); | ||
| } | ||
| // Mount identity (JWT bearer grant) first so OAuth token can fall through | ||
| if (opts.identity) { | ||
| const identityOptions = { | ||
| ...opts.identity, | ||
| basePath, | ||
| tokenPath: tokenPath ?? `${basePath}token`, | ||
| }; | ||
| mountIdentity(app, identityOptions); | ||
| } | ||
| // Mount OAuth endpoints functionally if an OAuth provider is present | ||
| if (isOAuthProvider(provider)) { | ||
| // Authorization endpoint | ||
| const authPath = authorizationPath ?? `${basePath}authorize`; | ||
| app.use(authPath, authorizationHandler({ provider })); | ||
| // Token endpoint (OAuth); identity's token handler will handle JWT grant and call next() otherwise | ||
| const tokPath = tokenPath ?? `${basePath}token`; | ||
| app.use(tokPath, tokenHandler({ provider })); | ||
| // Dynamic client registration if supported | ||
| if (provider.clientsStore?.registerClient) { | ||
| const regPath = registrationPath ?? `${basePath}register`; | ||
| app.use(regPath, clientRegistrationHandler({ clientsStore: provider.clientsStore })); | ||
| } | ||
| // Token revocation if supported | ||
| if (provider.revokeToken) { | ||
| const revPath = revocationPath ?? `${basePath}revoke`; | ||
| app.use(revPath, revocationHandler({ provider })); | ||
| } | ||
| // Optional OAuth callback | ||
| const callbackHandler = provider.handleOAuthCallback?.bind(provider); | ||
| if (callbackHandler) { | ||
| const callbackPath = provider.callbackPath ?? "/callback"; | ||
| app.get(callbackPath, async (req, res) => { | ||
| const code = typeof req.query.code === "string" ? req.query.code : undefined; | ||
| const state = typeof req.query.state === "string" ? req.query.state : undefined; | ||
| if (!code) { | ||
| res.status(400).send("Invalid request parameters"); | ||
| return; | ||
| } | ||
| try { | ||
| const redirectUrl = await callbackHandler(code, state, res); | ||
| res.redirect(redirectUrl.toString()); | ||
| } | ||
| catch (error) { | ||
| console.error(error); | ||
| res.status(500).send("Error during authentication callback"); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| // Protect MCP resource with bearer auth if a verifier/provider is present | ||
| if (provider) { | ||
| app.use("/mcp", (req, res, next) => { | ||
| return requireBearerAuth({ | ||
| verifier: provider, | ||
| requiredScopes: provider.requiredScopes, | ||
| resourceMetadataUrl: provider.resourceMetadataUrl, | ||
| })(req, res, next); | ||
| }); | ||
| } | ||
| } |
| export * from "./stateful.js"; | ||
| export * from "./stateless.js"; | ||
| export * from "./session.js"; | ||
| export * from "./auth/oauth.js"; | ||
| export * from "./auth/identity.js"; |
| export * from "./stateful.js"; | ||
| export * from "./stateless.js"; | ||
| export * from "./session.js"; | ||
| export * from "./auth/oauth.js"; | ||
| export * from "./auth/identity.js"; |
| /** | ||
| * Logger interface for structured logging | ||
| */ | ||
| export interface Logger { | ||
| info(msg: string, ...args: unknown[]): void; | ||
| info(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void; | ||
| error(msg: string, ...args: unknown[]): void; | ||
| error(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void; | ||
| warn(msg: string, ...args: unknown[]): void; | ||
| warn(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void; | ||
| debug(msg: string, ...args: unknown[]): void; | ||
| debug(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void; | ||
| } | ||
| type LogLevel = "debug" | "info" | "warn" | "error"; | ||
| /** | ||
| * Creates a simple console-based logger with pretty formatting | ||
| */ | ||
| export declare function createLogger(logLevel?: LogLevel): Logger; | ||
| export {}; |
| import chalk from "chalk"; | ||
| /** | ||
| * Lightweight stringify with depth limiting | ||
| */ | ||
| function stringifyWithDepth(obj, maxDepth = 3) { | ||
| let depth = 0; | ||
| const seen = new WeakSet(); | ||
| try { | ||
| return JSON.stringify(obj, (key, value) => { | ||
| // Track depth | ||
| if (key === "") | ||
| depth = 0; | ||
| else if (typeof value === "object" && value !== null) | ||
| depth++; | ||
| // Depth limit | ||
| if (depth > maxDepth) { | ||
| return "[Object]"; | ||
| } | ||
| // Circular reference check | ||
| if (typeof value === "object" && value !== null) { | ||
| if (seen.has(value)) | ||
| return "[Circular]"; | ||
| seen.add(value); | ||
| } | ||
| // Handle special types | ||
| if (typeof value === "function") | ||
| return "[Function]"; | ||
| if (typeof value === "bigint") | ||
| return `${value}n`; | ||
| if (value instanceof Error) | ||
| return { name: value.name, message: value.message }; | ||
| if (value instanceof Date) | ||
| return value.toISOString(); | ||
| return value; | ||
| }, 2); | ||
| } | ||
| catch { | ||
| return String(obj); | ||
| } | ||
| } | ||
| /** | ||
| * Creates a simple console-based logger with pretty formatting | ||
| */ | ||
| export function createLogger(logLevel = "info") { | ||
| const levels = { debug: 0, info: 1, warn: 2, error: 3 }; | ||
| const currentLevel = levels[logLevel]; | ||
| const formatLog = (level, color, msgOrObj, msg) => { | ||
| const time = new Date().toISOString().split("T")[1].split(".")[0]; | ||
| const timestamp = chalk.dim(time); | ||
| const levelStr = color(level); | ||
| if (typeof msgOrObj === "string") { | ||
| return `${timestamp} ${levelStr} ${msgOrObj}`; | ||
| } | ||
| const message = msg || ""; | ||
| const data = stringifyWithDepth(msgOrObj, 3); | ||
| return `${timestamp} ${levelStr} ${message}\n${chalk.dim(data)}`; | ||
| }; | ||
| return { | ||
| debug: (msgOrObj, msg) => { | ||
| if (currentLevel <= 0) | ||
| console.error(formatLog("DEBUG", chalk.cyan, msgOrObj, msg)); | ||
| }, | ||
| info: (msgOrObj, msg) => { | ||
| if (currentLevel <= 1) | ||
| console.error(formatLog("INFO", chalk.blue, msgOrObj, msg)); | ||
| }, | ||
| warn: (msgOrObj, msg) => { | ||
| if (currentLevel <= 2) | ||
| console.error(formatLog("WARN", chalk.yellow, msgOrObj, msg)); | ||
| }, | ||
| error: (msgOrObj, msg) => { | ||
| if (currentLevel <= 3) | ||
| console.error(formatLog("ERROR", chalk.red, msgOrObj, msg)); | ||
| }, | ||
| }; | ||
| } |
| import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; | ||
| export interface SessionStore<T extends Transport> { | ||
| /** return existing transport (or `undefined`) */ | ||
| get(id: string): T | undefined; | ||
| /** insert / update */ | ||
| set(id: string, t: T): void; | ||
| /** optional - explicit eviction */ | ||
| delete?(id: string): void; | ||
| } | ||
| /** | ||
| * Minimal Map‑based LRU implementation that fulfils {@link SessionStore}. | ||
| * Keeps at most `max` transports; upon insert, the least‑recently‑used entry | ||
| * (oldest insertion order) is removed and the evicted transport is closed. | ||
| * | ||
| * @param max maximum number of sessions to retain (default = 1000) | ||
| */ | ||
| export declare const createLRUStore: <T extends Transport>(max?: number) => SessionStore<T>; |
| /** | ||
| * Minimal Map‑based LRU implementation that fulfils {@link SessionStore}. | ||
| * Keeps at most `max` transports; upon insert, the least‑recently‑used entry | ||
| * (oldest insertion order) is removed and the evicted transport is closed. | ||
| * | ||
| * @param max maximum number of sessions to retain (default = 1000) | ||
| */ | ||
| export const createLRUStore = (max = 1000) => { | ||
| // 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. | ||
| const cache = new Map(); | ||
| return { | ||
| get: id => { | ||
| const t = cache.get(id); | ||
| if (!t) | ||
| return undefined; | ||
| // refresh position | ||
| cache.delete(id); | ||
| cache.set(id, t); | ||
| return t; | ||
| }, | ||
| set: (id, transport) => { | ||
| if (cache.has(id)) { | ||
| // key already present - refresh position | ||
| cache.delete(id); | ||
| } | ||
| else if (cache.size >= max) { | ||
| // evict oldest entry (first in insertion order) | ||
| const [lruId, lruTransport] = cache.entries().next().value; | ||
| lruTransport.close?.(); | ||
| cache.delete(lruId); | ||
| } | ||
| cache.set(id, transport); | ||
| }, | ||
| delete: id => cache.delete(id), | ||
| }; | ||
| }; |
| import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; | ||
| import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; | ||
| import express from "express"; | ||
| import type { z } from "zod"; | ||
| import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { type SessionStore } from "./session.js"; | ||
| import type { Logger } from "./logger.js"; | ||
| /** | ||
| * Arguments when we create a new instance of your server | ||
| */ | ||
| export interface CreateServerArg<T = Record<string, unknown>> { | ||
| sessionId: string; | ||
| config: T; | ||
| auth?: AuthInfo; | ||
| logger: Logger; | ||
| } | ||
| export type CreateServerFn<T = Record<string, unknown>> = (arg: CreateServerArg<T>) => McpServer["server"]; | ||
| /** | ||
| * Configuration options for the stateful server | ||
| */ | ||
| export interface StatefulServerOptions<T = Record<string, unknown>> { | ||
| /** | ||
| * Session store to use for managing active sessions | ||
| */ | ||
| sessionStore?: SessionStore<StreamableHTTPServerTransport>; | ||
| /** | ||
| * Zod schema for config validation | ||
| */ | ||
| schema?: z.ZodSchema<T>; | ||
| /** | ||
| * Express app instance to use (optional) | ||
| */ | ||
| app?: express.Application; | ||
| /** | ||
| * Log level for the server (default: 'info') | ||
| */ | ||
| logLevel?: "debug" | "info" | "warn" | "error"; | ||
| } | ||
| /** | ||
| * Creates a stateful server for handling MCP requests. | ||
| * For every new session, we invoke createMcpServer to create a new instance of the server. | ||
| * @param createMcpServer Function to create an MCP server | ||
| * @param options Configuration options including optional schema validation and Express app | ||
| * @returns Express app | ||
| */ | ||
| export declare function createStatefulServer<T = Record<string, unknown>>(createMcpServer: CreateServerFn<T>, options?: StatefulServerOptions<T>): { | ||
| app: express.Application; | ||
| }; |
| import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; | ||
| import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; | ||
| import express from "express"; | ||
| import { randomUUID } from "node:crypto"; | ||
| import { parseAndValidateConfig } from "../shared/config.js"; | ||
| import * as zod from "zod"; | ||
| import { createLRUStore } from "./session.js"; | ||
| import { createLogger } from "./logger.js"; | ||
| /** | ||
| * Creates a stateful server for handling MCP requests. | ||
| * For every new session, we invoke createMcpServer to create a new instance of the server. | ||
| * @param createMcpServer Function to create an MCP server | ||
| * @param options Configuration options including optional schema validation and Express app | ||
| * @returns Express app | ||
| */ | ||
| export function createStatefulServer(createMcpServer, options) { | ||
| const app = options?.app ?? express(); | ||
| app.use("/mcp", express.json()); | ||
| const sessionStore = options?.sessionStore ?? createLRUStore(); | ||
| const logger = createLogger(options?.logLevel ?? "info"); | ||
| // Handle POST requests for client-to-server communication | ||
| app.post("/mcp", async (req, res) => { | ||
| // Log incoming MCP request | ||
| logger.debug({ | ||
| method: req.body.method, | ||
| id: req.body.id, | ||
| sessionId: req.headers["mcp-session-id"], | ||
| }, "MCP Request"); | ||
| // Check for existing session ID | ||
| const sessionId = req.headers["mcp-session-id"]; | ||
| let transport; | ||
| if (sessionId && sessionStore.get(sessionId)) { | ||
| // Reuse existing transport | ||
| transport = sessionStore.get(sessionId); | ||
| } | ||
| else if (!sessionId && isInitializeRequest(req.body)) { | ||
| // New initialization request | ||
| const newSessionId = randomUUID(); | ||
| transport = new StreamableHTTPServerTransport({ | ||
| sessionIdGenerator: () => newSessionId, | ||
| onsessioninitialized: sessionId => { | ||
| // Store the transport by session ID | ||
| sessionStore.set(sessionId, transport); | ||
| }, | ||
| }); | ||
| // Clean up transport when closed | ||
| transport.onclose = () => { | ||
| if (transport.sessionId) { | ||
| sessionStore.delete?.(transport.sessionId); | ||
| } | ||
| }; | ||
| // New session - validate config | ||
| const configResult = parseAndValidateConfig(req, options?.schema); | ||
| if (!configResult.ok) { | ||
| const status = configResult.error.status || 400; | ||
| logger.error({ error: configResult.error, sessionId: newSessionId }, "Config validation failed"); | ||
| res.status(status).json(configResult.error); | ||
| return; | ||
| } | ||
| const config = configResult.value; | ||
| try { | ||
| logger.info({ sessionId: newSessionId }, "Creating new session"); | ||
| const server = createMcpServer({ | ||
| sessionId: newSessionId, | ||
| config: config, | ||
| auth: req.auth, | ||
| logger, | ||
| }); | ||
| // Connect to the MCP server | ||
| await server.connect(transport); | ||
| } | ||
| catch (error) { | ||
| logger.error({ error, sessionId: newSessionId }, "Error initializing server"); | ||
| res.status(500).json({ | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32603, | ||
| message: "Error initializing server.", | ||
| }, | ||
| id: null, | ||
| }); | ||
| return; | ||
| } | ||
| } | ||
| else { | ||
| // Invalid request | ||
| logger.warn({ sessionId }, "Session not found or expired"); | ||
| res.status(400).json({ | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32000, | ||
| message: "Session not found or expired", | ||
| }, | ||
| id: null, | ||
| }); | ||
| return; | ||
| } | ||
| // Handle the request | ||
| await transport.handleRequest(req, res, req.body); | ||
| // Log successful response | ||
| logger.debug({ | ||
| method: req.body.method, | ||
| id: req.body.id, | ||
| sessionId: req.headers["mcp-session-id"], | ||
| }, "MCP Response sent"); | ||
| }); | ||
| // Add .well-known/mcp-config endpoint for configuration discovery | ||
| app.get("/.well-known/mcp-config", (req, res) => { | ||
| // Set proper content type for JSON Schema | ||
| res.set("Content-Type", "application/schema+json; charset=utf-8"); | ||
| // Create schema with metadata using Zod's native .meta() | ||
| const schema = (options?.schema ?? zod.object({})).meta({ | ||
| title: "MCP Session Configuration", | ||
| description: "Schema for the /mcp endpoint configuration", | ||
| "x-query-style": "dot+bracket", | ||
| }); | ||
| const configSchema = { | ||
| ...zod.toJSONSchema(schema, { target: "draft-2020-12" }), | ||
| // $id is dynamic based on request, so we add it manually | ||
| $id: `${req.protocol}://${req.get("host")}/.well-known/mcp-config`, | ||
| }; | ||
| res.json(configSchema); | ||
| }); | ||
| // Handle GET requests for server-to-client notifications via SSE | ||
| app.get("/mcp", async (req, res) => { | ||
| const sessionId = req.headers["mcp-session-id"]; | ||
| if (!sessionId || !sessionStore.get(sessionId)) { | ||
| res.status(400).send("Invalid or expired session ID"); | ||
| return; | ||
| } | ||
| const transport = sessionStore.get(sessionId); | ||
| await transport.handleRequest(req, res); | ||
| }); | ||
| // Handle DELETE requests for session termination | ||
| // https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management | ||
| app.delete("/mcp", async (req, res) => { | ||
| const sessionId = req.headers["mcp-session-id"]; | ||
| if (!sessionId) { | ||
| logger.warn("Session termination request missing session ID"); | ||
| res.status(400).json({ | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32600, | ||
| message: "Missing mcp-session-id header", | ||
| }, | ||
| id: null, | ||
| }); | ||
| return; | ||
| } | ||
| const transport = sessionStore.get(sessionId); | ||
| if (!transport) { | ||
| logger.warn({ sessionId }, "Session termination failed - not found"); | ||
| res.status(404).json({ | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32000, | ||
| message: "Session not found or expired", | ||
| }, | ||
| id: null, | ||
| }); | ||
| return; | ||
| } | ||
| // Close the transport | ||
| transport.close?.(); | ||
| logger.info({ sessionId }, "Session terminated"); | ||
| // Acknowledge session termination with 204 No Content | ||
| res.status(204).end(); | ||
| }); | ||
| return { app }; | ||
| } |
| import express from "express"; | ||
| import type { z } from "zod"; | ||
| import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; | ||
| import type { OAuthMountOptions } from "./auth/oauth.js"; | ||
| import { type Logger } from "./logger.js"; | ||
| export type { Logger } from "./logger.js"; | ||
| /** | ||
| * Arguments when we create a stateless server instance | ||
| */ | ||
| export interface CreateStatelessServerArg<T = Record<string, unknown>> { | ||
| config: T; | ||
| auth?: AuthInfo; | ||
| logger: Logger; | ||
| } | ||
| export type CreateStatelessServerFn<T = Record<string, unknown>> = (arg: CreateStatelessServerArg<T>) => McpServer["server"]; | ||
| /** | ||
| * Configuration options for the stateless server | ||
| */ | ||
| export interface StatelessServerOptions<T = Record<string, unknown>> { | ||
| /** | ||
| * Zod schema for config validation | ||
| */ | ||
| schema?: z.ZodSchema<T>; | ||
| /** | ||
| * Express app instance to use (optional) | ||
| */ | ||
| app?: express.Application; | ||
| oauth?: OAuthMountOptions; | ||
| /** | ||
| * Log level for the server (default: 'info') | ||
| */ | ||
| logLevel?: "debug" | "info" | "warn" | "error"; | ||
| } | ||
| /** | ||
| * Creates a stateless server for handling MCP requests. | ||
| * Each request creates a new server instance - no session state is maintained. | ||
| * This is ideal for stateless API integrations and serverless environments. | ||
| * | ||
| * @param createMcpServer Function to create an MCP server | ||
| * @param options Configuration options including optional schema validation and Express app | ||
| * @returns Express app | ||
| */ | ||
| export declare function createStatelessServer<T = Record<string, unknown>>(createMcpServer: CreateStatelessServerFn<T>, options?: StatelessServerOptions<T>): { | ||
| app: express.Application; | ||
| }; |
| import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; | ||
| import express from "express"; | ||
| import * as zod from "zod"; | ||
| import { parseAndValidateConfig } from "../shared/config.js"; | ||
| import { createLogger } from "./logger.js"; | ||
| /** | ||
| * Creates a stateless server for handling MCP requests. | ||
| * Each request creates a new server instance - no session state is maintained. | ||
| * This is ideal for stateless API integrations and serverless environments. | ||
| * | ||
| * @param createMcpServer Function to create an MCP server | ||
| * @param options Configuration options including optional schema validation and Express app | ||
| * @returns Express app | ||
| */ | ||
| export function createStatelessServer(createMcpServer, options) { | ||
| const app = options?.app ?? express(); | ||
| const logger = createLogger(options?.logLevel ?? "info"); | ||
| app.use("/mcp", express.json()); | ||
| // Handle POST requests for client-to-server communication | ||
| app.post("/mcp", async (req, res) => { | ||
| // In stateless mode, create a new instance of transport and server for each request | ||
| // to ensure complete isolation. A single instance would cause request ID collisions | ||
| // when multiple clients connect concurrently. | ||
| try { | ||
| // Log incoming MCP request | ||
| logger.debug({ | ||
| method: req.body.method, | ||
| id: req.body.id, | ||
| params: req.body.params, | ||
| }, "MCP Request"); | ||
| // Validate config for all requests in stateless mode | ||
| const configResult = parseAndValidateConfig(req, options?.schema); | ||
| if (!configResult.ok) { | ||
| const status = configResult.error.status || 400; | ||
| logger.error({ error: configResult.error }, "Config validation failed"); | ||
| res.status(status).json(configResult.error); | ||
| return; | ||
| } | ||
| const config = configResult.value; | ||
| // Create a fresh server instance for each request | ||
| const server = createMcpServer({ | ||
| config, | ||
| auth: req.auth, | ||
| logger, | ||
| }); | ||
| // Create a new transport for this request (no session management) | ||
| const transport = new StreamableHTTPServerTransport({ | ||
| sessionIdGenerator: undefined, | ||
| }); | ||
| // Clean up resources when request closes | ||
| res.on("close", () => { | ||
| transport.close(); | ||
| server.close(); | ||
| }); | ||
| // Connect to the MCP server | ||
| await server.connect(transport); | ||
| // Handle the request directly | ||
| await transport.handleRequest(req, res, req.body); | ||
| // Log successful response | ||
| logger.debug({ | ||
| method: req.body.method, | ||
| id: req.body.id, | ||
| }, "MCP Response sent"); | ||
| } | ||
| catch (error) { | ||
| logger.error({ error }, "Error handling MCP request"); | ||
| if (!res.headersSent) { | ||
| res.status(500).json({ | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32603, | ||
| message: "Internal server error", | ||
| }, | ||
| id: null, | ||
| }); | ||
| } | ||
| } | ||
| }); | ||
| // SSE notifications not supported in stateless mode | ||
| app.get("/mcp", async (_req, res) => { | ||
| res.status(405).json({ | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32000, | ||
| message: "Method not allowed.", | ||
| }, | ||
| id: null, | ||
| }); | ||
| }); | ||
| // Session termination not needed in stateless mode | ||
| app.delete("/mcp", async (_req, res) => { | ||
| res.status(405).json({ | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32000, | ||
| message: "Method not allowed.", | ||
| }, | ||
| id: null, | ||
| }); | ||
| }); | ||
| // Add .well-known/mcp-config endpoint for configuration discovery | ||
| app.get("/.well-known/mcp-config", (req, res) => { | ||
| // Set proper content type for JSON Schema | ||
| res.set("Content-Type", "application/schema+json; charset=utf-8"); | ||
| // Create schema with metadata using Zod's native .meta() | ||
| const schema = (options?.schema ?? zod.object({})).meta({ | ||
| title: "MCP Session Configuration", | ||
| description: "Schema for the /mcp endpoint configuration", | ||
| "x-query-style": "dot+bracket", | ||
| }); | ||
| const configSchema = { | ||
| ...zod.toJSONSchema(schema, { target: "draft-2020-12" }), | ||
| // $id is dynamic based on request, so we add it manually | ||
| $id: `${req.protocol}://${req.get("host")}/.well-known/mcp-config`, | ||
| }; | ||
| res.json(configSchema); | ||
| }); | ||
| return { app }; | ||
| } |
| import type { Request as ExpressRequest } from "express"; | ||
| import * as z from "zod"; | ||
| export interface SmitheryUrlOptions { | ||
| apiKey?: string; | ||
| profile?: string; | ||
| config?: object; | ||
| } | ||
| export declare function appendConfigAsDotParams(url: URL, config: unknown): void; | ||
| /** | ||
| * Creates a URL to connect to the Smithery MCP server. | ||
| * @param baseUrl The base URL of the Smithery server | ||
| * @param options Optional configuration object | ||
| * @returns A URL with config encoded using dot-notation query params (e.g. model.name=gpt-4&debug=true) | ||
| */ | ||
| export declare function createSmitheryUrl(baseUrl: string, options?: SmitheryUrlOptions): URL; | ||
| /** | ||
| * Parses and validates config from an Express request with optional Zod schema validation | ||
| * Supports dot-notation config parameters (e.g., foo=bar, a.b=c) | ||
| * @param req The express request | ||
| * @param schema Optional Zod schema for validation | ||
| * @returns Result with either parsed data or error response | ||
| */ | ||
| export declare function parseAndValidateConfig<T = Record<string, unknown>>(req: ExpressRequest, schema?: z.ZodSchema<T>): import("okay-error").Err<{ | ||
| readonly title: "Invalid configuration parameters"; | ||
| readonly status: 422; | ||
| readonly detail: "One or more config parameters are invalid."; | ||
| readonly instance: string; | ||
| readonly configSchema: z.core.ZodStandardJSONSchemaPayload<z.ZodType<T, unknown, z.core.$ZodTypeInternals<T, unknown>>>; | ||
| readonly errors: { | ||
| param: string; | ||
| pointer: string; | ||
| reason: string; | ||
| received: unknown; | ||
| }[]; | ||
| readonly help: "Pass config as URL query params. Example: /mcp?param1=value1¶m2=value2"; | ||
| }> | import("okay-error").Ok<T>; | ||
| export declare function parseConfigFromQuery(query: Iterable<[string, unknown]>): Record<string, unknown>; |
| import _ from "lodash"; | ||
| import { err, ok } from "okay-error"; | ||
| import * as z from "zod"; | ||
| function isPlainObject(value) { | ||
| return value !== null && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
| export function appendConfigAsDotParams(url, config) { | ||
| function add(pathParts, value) { | ||
| if (Array.isArray(value)) { | ||
| for (let index = 0; index < value.length; index++) { | ||
| add([...pathParts, String(index)], value[index]); | ||
| } | ||
| return; | ||
| } | ||
| if (isPlainObject(value)) { | ||
| for (const [key, nested] of Object.entries(value)) { | ||
| add([...pathParts, key], nested); | ||
| } | ||
| return; | ||
| } | ||
| const key = pathParts.join("."); | ||
| let stringValue; | ||
| switch (typeof value) { | ||
| case "string": | ||
| stringValue = value; | ||
| break; | ||
| case "number": | ||
| case "boolean": | ||
| stringValue = String(value); | ||
| break; | ||
| default: | ||
| stringValue = JSON.stringify(value); | ||
| } | ||
| url.searchParams.set(key, stringValue); | ||
| } | ||
| if (isPlainObject(config)) { | ||
| for (const [key, value] of Object.entries(config)) { | ||
| add([key], value); | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Creates a URL to connect to the Smithery MCP server. | ||
| * @param baseUrl The base URL of the Smithery server | ||
| * @param options Optional configuration object | ||
| * @returns A URL with config encoded using dot-notation query params (e.g. model.name=gpt-4&debug=true) | ||
| */ | ||
| export function createSmitheryUrl(baseUrl, options) { | ||
| const url = new URL(`${baseUrl}/mcp`); | ||
| if (options?.config) { | ||
| appendConfigAsDotParams(url, options.config); | ||
| } | ||
| if (options?.apiKey) { | ||
| url.searchParams.set("api_key", options.apiKey); | ||
| } | ||
| if (options?.profile) { | ||
| url.searchParams.set("profile", options.profile); | ||
| } | ||
| return url; | ||
| } | ||
| /** | ||
| * Parses and validates config from an Express request with optional Zod schema validation | ||
| * Supports dot-notation config parameters (e.g., foo=bar, a.b=c) | ||
| * @param req The express request | ||
| * @param schema Optional Zod schema for validation | ||
| * @returns Result with either parsed data or error response | ||
| */ | ||
| export function parseAndValidateConfig(req, schema) { | ||
| const config = parseConfigFromQuery(Object.entries(req.query)); | ||
| // Validate config against schema if provided | ||
| if (schema) { | ||
| const result = schema.safeParse(config); | ||
| if (!result.success) { | ||
| const jsonSchema = z.toJSONSchema(schema); | ||
| const errors = result.error.issues.map(issue => { | ||
| // Safely traverse the config object to get the received value | ||
| let received = config; | ||
| for (const key of issue.path) { | ||
| const keyStr = String(key); | ||
| if (received && typeof received === "object" && keyStr in received) { | ||
| received = received[keyStr]; | ||
| } | ||
| else { | ||
| received = undefined; | ||
| break; | ||
| } | ||
| } | ||
| return { | ||
| param: issue.path.join(".") || "root", | ||
| pointer: `/${issue.path.join("/")}`, | ||
| reason: issue.message, | ||
| received, | ||
| }; | ||
| }); | ||
| return err({ | ||
| title: "Invalid configuration parameters", | ||
| status: 422, | ||
| detail: "One or more config parameters are invalid.", | ||
| instance: req.originalUrl, | ||
| configSchema: jsonSchema, | ||
| errors, | ||
| help: "Pass config as URL query params. Example: /mcp?param1=value1¶m2=value2", | ||
| }); | ||
| } | ||
| return ok(result.data); | ||
| } | ||
| return ok(config); | ||
| } | ||
| // Process dot-notation config parameters from query parameters (foo=bar, a.b=c) | ||
| // This allows URL params like ?server.host=localhost&server.port=8080&debug=true | ||
| export function parseConfigFromQuery(query) { | ||
| const config = {}; | ||
| for (const [key, value] of query) { | ||
| // Skip reserved parameters | ||
| if (key === "api_key" || key === "profile") | ||
| continue; | ||
| const pathParts = key.split("."); | ||
| // Handle array values from Express query parsing | ||
| const rawValue = Array.isArray(value) ? value[0] : value; | ||
| if (typeof rawValue !== "string") | ||
| continue; | ||
| // Try to parse value as JSON (for booleans, numbers, objects) | ||
| let parsedValue = rawValue; | ||
| try { | ||
| parsedValue = JSON.parse(rawValue); | ||
| } | ||
| catch { | ||
| // If parsing fails, use the raw string value | ||
| } | ||
| // Use lodash's set method to handle nested paths | ||
| _.set(config, pathParts, parsedValue); | ||
| } | ||
| return config; | ||
| } |
| /** | ||
| * Patches a function on an object | ||
| * @param obj | ||
| * @param key | ||
| * @param patcher | ||
| */ | ||
| export declare function patch<T extends { | ||
| [P in K]: (...args: any[]) => any; | ||
| }, K extends keyof T & string>(obj: T, key: K, patcher: (fn: T[K]) => T[K]): void; | ||
| export declare function patch<T extends { | ||
| [P in K]?: (...args: any[]) => any; | ||
| }, K extends keyof T & string>(obj: T, key: K, patcher: (fn?: T[K]) => T[K]): void; |
| /** | ||
| * Patches a function on an object | ||
| * @param obj | ||
| * @param key | ||
| * @param patcher | ||
| */ | ||
| // Unified implementation (not type-checked by callers) | ||
| export function patch(obj, key, patcher) { | ||
| // If the property is actually a function, bind it; otherwise undefined | ||
| const original = typeof obj[key] === "function" ? obj[key].bind(obj) : undefined; | ||
| obj[key] = patcher(original); | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
2
-71.43%4
-60%0
-100%5737
-86.98%14
-36.36%132
-86.88%7
-85.71%1
Infinity%- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed