From 869a8aa557dfae546e648a5d92e86432703c2cff Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 05:39:42 +0000 Subject: [PATCH 01/10] chore(mcp): forward STAINLESS_API_KEY to docs search endpoint --- packages/mcp-server/src/docs-search-tool.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index 078b09f..8bfc3cb 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -1,6 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { Metadata, asTextContentResult } from './types'; +import { readEnv } from './server'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -45,7 +46,12 @@ const docsSearchURL = export const handler = async (_: unknown, args: Record | undefined) => { const body = args as any; const query = new URLSearchParams(body).toString(); - const result = await fetch(`${docsSearchURL}?${query}`); + const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); + const result = await fetch(`${docsSearchURL}?${query}`, { + headers: { + ...(stainlessAPIKey && { Authorization: stainlessAPIKey }), + }, + }); if (!result.ok) { throw new Error( From 01a45632c1ee8e9eec2a3995a1b7891059d1b101 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 05:42:00 +0000 Subject: [PATCH 02/10] chore(internal): improve layout of generated MCP server files --- .../mcp-server/src/{headers.ts => auth.ts} | 0 packages/mcp-server/src/code-tool.ts | 2 +- packages/mcp-server/src/docs-search-tool.ts | 2 +- packages/mcp-server/src/http.ts | 5 ++-- packages/mcp-server/src/methods.ts | 2 ++ packages/mcp-server/src/options.ts | 2 ++ packages/mcp-server/src/server.ts | 28 +------------------ packages/mcp-server/src/util.ts | 25 +++++++++++++++++ 8 files changed, 35 insertions(+), 31 deletions(-) rename packages/mcp-server/src/{headers.ts => auth.ts} (100%) create mode 100644 packages/mcp-server/src/util.ts diff --git a/packages/mcp-server/src/headers.ts b/packages/mcp-server/src/auth.ts similarity index 100% rename from packages/mcp-server/src/headers.ts rename to packages/mcp-server/src/auth.ts diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 25d04c6..5c32c60 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -2,7 +2,7 @@ import { McpTool, Metadata, ToolCallResult, asErrorResult, asTextContentResult } from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import { readEnv, requireValue } from './server'; +import { readEnv, requireValue } from './util'; import { WorkerInput, WorkerOutput } from './code-tool-types'; import { SdkMethod } from './methods'; import { SendblueAPI } from 'sendblue'; diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index 8bfc3cb..e110c73 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { Metadata, asTextContentResult } from './types'; -import { readEnv } from './server'; +import { readEnv } from './util'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index 8ca827c..4940a6b 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -2,12 +2,13 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { ClientOptions } from 'sendblue'; import express from 'express'; import morgan from 'morgan'; import morganBody from 'morgan-body'; +import { parseAuthHeaders } from './auth'; import { McpOptions } from './options'; -import { ClientOptions, initMcpServer, newMcpServer } from './server'; -import { parseAuthHeaders } from './headers'; +import { initMcpServer, newMcpServer } from './server'; const newServer = async ({ clientOptions, diff --git a/packages/mcp-server/src/methods.ts b/packages/mcp-server/src/methods.ts index 52f99cf..9424c8d 100644 --- a/packages/mcp-server/src/methods.ts +++ b/packages/mcp-server/src/methods.ts @@ -1,3 +1,5 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + import { McpOptions } from './options'; export type SdkMethod = { diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 92d1b07..cfde21d 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -1,3 +1,5 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + import qs from 'qs'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index f128f02..c739e45 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -14,9 +14,7 @@ import docsSearchTool from './docs-search-tool'; import { McpOptions } from './options'; import { blockedMethodsForCodeTool } from './methods'; import { HandlerFunction, McpTool } from './types'; - -export { McpOptions } from './options'; -export { ClientOptions } from 'sendblue'; +import { readEnv } from './util'; async function getInstructions() { // This API key is optional; providing it allows the server to fetch instructions for unreleased versions. @@ -169,27 +167,3 @@ export async function executeHandler( ) { return await handler(client, args || {}); } - -export const readEnv = (env: string): string | undefined => { - if (typeof (globalThis as any).process !== 'undefined') { - return (globalThis as any).process.env?.[env]?.trim(); - } else if (typeof (globalThis as any).Deno !== 'undefined') { - return (globalThis as any).Deno.env?.get?.(env)?.trim(); - } - return; -}; - -export const readEnvOrError = (env: string): string => { - let envValue = readEnv(env); - if (envValue === undefined) { - throw new Error(`Environment variable ${env} is not set`); - } - return envValue; -}; - -export const requireValue = (value: T | undefined, description: string): T => { - if (value === undefined) { - throw new Error(`Missing required value: ${description}`); - } - return value; -}; diff --git a/packages/mcp-server/src/util.ts b/packages/mcp-server/src/util.ts new file mode 100644 index 0000000..40ed550 --- /dev/null +++ b/packages/mcp-server/src/util.ts @@ -0,0 +1,25 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export const readEnv = (env: string): string | undefined => { + if (typeof (globalThis as any).process !== 'undefined') { + return (globalThis as any).process.env?.[env]?.trim(); + } else if (typeof (globalThis as any).Deno !== 'undefined') { + return (globalThis as any).Deno.env?.get?.(env)?.trim(); + } + return; +}; + +export const readEnvOrError = (env: string): string => { + let envValue = readEnv(env); + if (envValue === undefined) { + throw new Error(`Environment variable ${env} is not set`); + } + return envValue; +}; + +export const requireValue = (value: T | undefined, description: string): T => { + if (value === undefined) { + throw new Error(`Missing required value: ${description}`); + } + return value; +}; From 06364085dd59f4b43b2f0f95b8bab83435fa9ffb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 05:18:02 +0000 Subject: [PATCH 03/10] chore(internal/client): fix form-urlencoded requests --- src/client.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client.ts b/src/client.ts index 0260ed4..377a997 100644 --- a/src/client.ts +++ b/src/client.ts @@ -764,6 +764,14 @@ export class SendblueAPI { (Symbol.iterator in body && 'next' in body && typeof body.next === 'function')) ) { return { bodyHeaders: undefined, body: Shims.ReadableStreamFrom(body as AsyncIterable) }; + } else if ( + typeof body === 'object' && + headers.values.get('content-type') === 'application/x-www-form-urlencoded' + ) { + return { + bodyHeaders: { 'content-type': 'application/x-www-form-urlencoded' }, + body: this.stringifyQuery(body as Record), + }; } else { return this.#encoder({ body, headers }); } From cdbf955893b151aa89778d292b3e041a476e0306 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 05:22:04 +0000 Subject: [PATCH 04/10] chore(internal): allow setting x-stainless-api-key header on mcp server requests --- packages/mcp-server/src/auth.ts | 17 ++++++++- packages/mcp-server/src/code-tool.ts | 33 ++++++++++------- packages/mcp-server/src/docs-search-tool.ts | 15 ++++---- packages/mcp-server/src/http.ts | 21 +++++++---- packages/mcp-server/src/options.ts | 9 +++++ packages/mcp-server/src/server.ts | 40 +++++++++++++-------- packages/mcp-server/src/stdio.ts | 4 +-- packages/mcp-server/src/types.ts | 16 ++++++--- 8 files changed, 109 insertions(+), 46 deletions(-) diff --git a/packages/mcp-server/src/auth.ts b/packages/mcp-server/src/auth.ts index dde3306..a4eff23 100644 --- a/packages/mcp-server/src/auth.ts +++ b/packages/mcp-server/src/auth.ts @@ -2,8 +2,9 @@ import { IncomingMessage } from 'node:http'; import { ClientOptions } from 'sendblue'; +import { McpOptions } from './options'; -export const parseAuthHeaders = (req: IncomingMessage, required?: boolean): Partial => { +export const parseClientAuthHeaders = (req: IncomingMessage, required?: boolean): Partial => { const apiKey = Array.isArray(req.headers['sb-api-key-id']) ? req.headers['sb-api-key-id'][0] @@ -14,3 +15,17 @@ export const parseAuthHeaders = (req: IncomingMessage, required?: boolean): Part : req.headers['sb-api-secret-key']; return { apiKey, apiSecret }; }; + +export const getStainlessApiKey = (req: IncomingMessage, mcpOptions: McpOptions): string | undefined => { + // Try to get the key from the x-stainless-api-key header + const headerKey = + Array.isArray(req.headers['x-stainless-api-key']) ? + req.headers['x-stainless-api-key'][0] + : req.headers['x-stainless-api-key']; + if (headerKey && typeof headerKey === 'string') { + return headerKey; + } + + // Fall back to value set in the mcpOptions (e.g. from environment variable), if provided + return mcpOptions.stainlessApiKey; +}; diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 5c32c60..fd36d75 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -1,11 +1,17 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { McpTool, Metadata, ToolCallResult, asErrorResult, asTextContentResult } from './types'; +import { + McpRequestContext, + McpTool, + Metadata, + ToolCallResult, + asErrorResult, + asTextContentResult, +} from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { readEnv, requireValue } from './util'; import { WorkerInput, WorkerOutput } from './code-tool-types'; import { SdkMethod } from './methods'; -import { SendblueAPI } from 'sendblue'; const prompt = `Runs JavaScript code to interact with the Sendblue API API. @@ -40,7 +46,7 @@ Variables will not persist between calls, so make sure to return or log any data * * @param endpoints - The endpoints to include in the list. */ -export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): McpTool { +export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | undefined }): McpTool { const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] }; const tool: Tool = { name: 'execute', @@ -60,19 +66,24 @@ export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): M required: ['code'], }, }; - const handler = async (client: SendblueAPI, args: any): Promise => { + const handler = async ({ + reqContext, + args, + }: { + reqContext: McpRequestContext; + args: any; + }): Promise => { const code = args.code as string; const intent = args.intent as string | undefined; + const client = reqContext.client; // Do very basic blocking of code that includes forbidden method names. // // WARNING: This is not secure against obfuscation and other evasion methods. If // stronger security blocks are required, then these should be enforced in the downstream // API (e.g., by having users call the MCP server with API keys with limited permissions). - if (params.blockedMethods) { - const blockedMatches = params.blockedMethods.filter((method) => - code.includes(method.fullyQualifiedName), - ); + if (blockedMethods) { + const blockedMatches = blockedMethods.filter((method) => code.includes(method.fullyQualifiedName)); if (blockedMatches.length > 0) { return asErrorResult( `The following methods have been blocked by the MCP server and cannot be used in code execution: ${blockedMatches @@ -82,16 +93,14 @@ export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): M } } - // this is not required, but passing a Stainless API key for the matching project_name - // will allow you to run code-mode queries against non-published versions of your SDK. - const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); const codeModeEndpoint = readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool'; + // Setting a Stainless API key authenticates requests to the code tool endpoint. const res = await fetch(codeModeEndpoint, { method: 'POST', headers: { - ...(stainlessAPIKey && { Authorization: stainlessAPIKey }), + ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), 'Content-Type': 'application/json', client_envs: JSON.stringify({ SENDBLUE_API_API_KEY: requireValue( diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index e110c73..ec49dcf 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -1,8 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from './types'; -import { readEnv } from './util'; - +import { Metadata, McpRequestContext, asTextContentResult } from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; export const metadata: Metadata = { @@ -43,13 +41,18 @@ export const tool: Tool = { const docsSearchURL = process.env['DOCS_SEARCH_URL'] || 'https://api.stainless.com/api/projects/sendblue-api/docs/search'; -export const handler = async (_: unknown, args: Record | undefined) => { +export const handler = async ({ + reqContext, + args, +}: { + reqContext: McpRequestContext; + args: Record | undefined; +}) => { const body = args as any; const query = new URLSearchParams(body).toString(); - const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); const result = await fetch(`${docsSearchURL}?${query}`, { headers: { - ...(stainlessAPIKey && { Authorization: stainlessAPIKey }), + ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), }, }); diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index 4940a6b..2a0c705 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -6,7 +6,7 @@ import { ClientOptions } from 'sendblue'; import express from 'express'; import morgan from 'morgan'; import morganBody from 'morgan-body'; -import { parseAuthHeaders } from './auth'; +import { getStainlessApiKey, parseClientAuthHeaders } from './auth'; import { McpOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; @@ -21,10 +21,12 @@ const newServer = async ({ req: express.Request; res: express.Response; }): Promise => { - const server = await newMcpServer(); + const stainlessApiKey = getStainlessApiKey(req, mcpOptions); + const server = await newMcpServer(stainlessApiKey); try { - const authOptions = parseAuthHeaders(req, false); + const authOptions = parseClientAuthHeaders(req, false); + await initMcpServer({ server: server, mcpOptions: mcpOptions, @@ -32,6 +34,7 @@ const newServer = async ({ ...clientOptions, ...authOptions, }, + stainlessApiKey: stainlessApiKey, }); } catch (error) { res.status(401).json({ @@ -112,13 +115,17 @@ export const streamableHTTPApp = ({ return app; }; -export const launchStreamableHTTPServer = async (params: { +export const launchStreamableHTTPServer = async ({ + mcpOptions, + debug, + port, +}: { mcpOptions: McpOptions; debug: boolean; port: number | string | undefined; }) => { - const app = streamableHTTPApp({ mcpOptions: params.mcpOptions, debug: params.debug }); - const server = app.listen(params.port); + const app = streamableHTTPApp({ mcpOptions, debug }); + const server = app.listen(port); const address = server.address(); if (typeof address === 'string') { @@ -126,6 +133,6 @@ export const launchStreamableHTTPServer = async (params: { } else if (address !== null) { console.error(`MCP Server running on streamable HTTP on port ${address.port}`); } else { - console.error(`MCP Server running on streamable HTTP on port ${params.port}`); + console.error(`MCP Server running on streamable HTTP on port ${port}`); } }; diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index cfde21d..32a8871 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -4,6 +4,7 @@ import qs from 'qs'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import z from 'zod'; +import { readEnv } from './util'; export type CLIOptions = McpOptions & { debug: boolean; @@ -14,6 +15,7 @@ export type CLIOptions = McpOptions & { export type McpOptions = { includeDocsTools?: boolean | undefined; + stainlessApiKey?: string | undefined; codeAllowHttpGets?: boolean | undefined; codeAllowedMethods?: string[] | undefined; codeBlockedMethods?: string[] | undefined; @@ -51,6 +53,12 @@ export function parseCLIOptions(): CLIOptions { description: 'Port to serve on if using http transport', }) .option('socket', { type: 'string', description: 'Unix socket to serve on if using http transport' }) + .option('stainless-api-key', { + type: 'string', + default: readEnv('STAINLESS_API_KEY'), + description: + 'API key for Stainless. Used to authenticate requests to Stainless-hosted tools endpoints.', + }) .option('tools', { type: 'string', array: true, @@ -81,6 +89,7 @@ export function parseCLIOptions(): CLIOptions { return { ...(includeDocsTools !== undefined && { includeDocsTools }), debug: !!argv.debug, + stainlessApiKey: argv.stainlessApiKey, codeAllowHttpGets: argv.codeAllowHttpGets, codeAllowedMethods: argv.codeAllowedMethods, codeBlockedMethods: argv.codeBlockedMethods, diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index c739e45..53c36b1 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -13,17 +13,17 @@ import { codeTool } from './code-tool'; import docsSearchTool from './docs-search-tool'; import { McpOptions } from './options'; import { blockedMethodsForCodeTool } from './methods'; -import { HandlerFunction, McpTool } from './types'; +import { HandlerFunction, McpRequestContext, ToolCallResult, McpTool } from './types'; import { readEnv } from './util'; -async function getInstructions() { - // This API key is optional; providing it allows the server to fetch instructions for unreleased versions. - const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); +async function getInstructions(stainlessApiKey: string | undefined): Promise { + // Setting the stainless API key is optional, but may be required + // to authenticate requests to the Stainless API. const response = await fetch( readEnv('CODE_MODE_INSTRUCTIONS_URL') ?? 'https://api.stainless.com/api/ai/instructions/sendblue-api', { method: 'GET', - headers: { ...(stainlessAPIKey && { Authorization: stainlessAPIKey }) }, + headers: { ...(stainlessApiKey && { Authorization: stainlessApiKey }) }, }, ); @@ -52,14 +52,14 @@ async function getInstructions() { return instructions; } -export const newMcpServer = async () => +export const newMcpServer = async (stainlessApiKey: string | undefined) => new McpServer( { name: 'sendblue_api', version: '3.1.1', }, { - instructions: await getInstructions(), + instructions: await getInstructions(stainlessApiKey), capabilities: { tools: {}, logging: {} }, }, ); @@ -72,6 +72,7 @@ export async function initMcpServer(params: { server: Server | McpServer; clientOptions?: ClientOptions; mcpOptions?: McpOptions; + stainlessApiKey?: string | undefined; }) { const server = params.server instanceof McpServer ? params.server.server : params.server; @@ -115,7 +116,14 @@ export async function initMcpServer(params: { throw new Error(`Unknown tool: ${name}`); } - return executeHandler(mcpTool.handler, client, args); + return executeHandler({ + handler: mcpTool.handler, + reqContext: { + client, + stainlessApiKey: params.stainlessApiKey ?? params.mcpOptions?.stainlessApiKey, + }, + args, + }); }); server.setRequestHandler(SetLevelRequestSchema, async (request) => { @@ -160,10 +168,14 @@ export function selectTools(options?: McpOptions): McpTool[] { /** * Runs the provided handler with the given client and arguments. */ -export async function executeHandler( - handler: HandlerFunction, - client: SendblueAPI, - args: Record | undefined, -) { - return await handler(client, args || {}); +export async function executeHandler({ + handler, + reqContext, + args, +}: { + handler: HandlerFunction; + reqContext: McpRequestContext; + args: Record | undefined; +}): Promise { + return await handler({ reqContext, args: args || {} }); } diff --git a/packages/mcp-server/src/stdio.ts b/packages/mcp-server/src/stdio.ts index 57b9912..ceccaed 100644 --- a/packages/mcp-server/src/stdio.ts +++ b/packages/mcp-server/src/stdio.ts @@ -3,9 +3,9 @@ import { McpOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; export const launchStdioServer = async (mcpOptions: McpOptions) => { - const server = await newMcpServer(); + const server = await newMcpServer(mcpOptions.stainlessApiKey); - await initMcpServer({ server, mcpOptions }); + await initMcpServer({ server, mcpOptions, stainlessApiKey: mcpOptions.stainlessApiKey }); const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/packages/mcp-server/src/types.ts b/packages/mcp-server/src/types.ts index 33593bf..b2a3b9f 100644 --- a/packages/mcp-server/src/types.ts +++ b/packages/mcp-server/src/types.ts @@ -42,10 +42,18 @@ export type ToolCallResult = { isError?: boolean; }; -export type HandlerFunction = ( - client: SendblueAPI, - args: Record | undefined, -) => Promise; +export type McpRequestContext = { + client: SendblueAPI; + stainlessApiKey?: string | undefined; +}; + +export type HandlerFunction = ({ + reqContext, + args, +}: { + reqContext: McpRequestContext; + args: Record | undefined; +}) => Promise; export function asTextContentResult(result: unknown): ToolCallResult { return { From 3c5591018ee53d8e51d94100f6cafa0c02572046 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 05:18:19 +0000 Subject: [PATCH 05/10] chore(internal): cache fetch instruction calls in MCP server --- packages/mcp-server/src/instructions.ts | 74 +++++++++++++++++++++++++ packages/mcp-server/src/server.ts | 38 +------------ 2 files changed, 75 insertions(+), 37 deletions(-) create mode 100644 packages/mcp-server/src/instructions.ts diff --git a/packages/mcp-server/src/instructions.ts b/packages/mcp-server/src/instructions.ts new file mode 100644 index 0000000..bad350d --- /dev/null +++ b/packages/mcp-server/src/instructions.ts @@ -0,0 +1,74 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { readEnv } from './util'; + +const INSTRUCTIONS_CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes + +interface InstructionsCacheEntry { + fetchedInstructions: string; + fetchedAt: number; +} + +const instructionsCache = new Map(); + +// Periodically evict stale entries so the cache doesn't grow unboundedly. +const _cacheCleanupInterval = setInterval(() => { + const now = Date.now(); + for (const [key, entry] of instructionsCache) { + if (now - entry.fetchedAt > INSTRUCTIONS_CACHE_TTL_MS) { + instructionsCache.delete(key); + } + } +}, INSTRUCTIONS_CACHE_TTL_MS); + +// Don't keep the process alive just for cleanup. +_cacheCleanupInterval.unref(); + +export async function getInstructions(stainlessApiKey: string | undefined): Promise { + const cacheKey = stainlessApiKey ?? ''; + const cached = instructionsCache.get(cacheKey); + + if (cached && Date.now() - cached.fetchedAt <= INSTRUCTIONS_CACHE_TTL_MS) { + return cached.fetchedInstructions; + } + + const fetchedInstructions = await fetchLatestInstructions(stainlessApiKey); + instructionsCache.set(cacheKey, { fetchedInstructions, fetchedAt: Date.now() }); + return fetchedInstructions; +} + +async function fetchLatestInstructions(stainlessApiKey: string | undefined): Promise { + // Setting the stainless API key is optional, but may be required + // to authenticate requests to the Stainless API. + const response = await fetch( + readEnv('CODE_MODE_INSTRUCTIONS_URL') ?? 'https://api.stainless.com/api/ai/instructions/sendblue-api', + { + method: 'GET', + headers: { ...(stainlessApiKey && { Authorization: stainlessApiKey }) }, + }, + ); + + let instructions: string | undefined; + if (!response.ok) { + console.warn( + 'Warning: failed to retrieve MCP server instructions. Proceeding with default instructions...', + ); + + instructions = ` + This is the sendblue-api MCP server. You will use Code Mode to help the user perform + actions. You can use search_docs tool to learn about how to take action with this server. Then, + you will write TypeScript code using the execute tool take action. It is CRITICAL that you be + thoughtful and deliberate when executing code. Always try to entirely solve the problem in code + block: it can be as long as you need to get the job done! + `; + } + + instructions ??= ((await response.json()) as { instructions: string }).instructions; + instructions = ` + If needed, you can get the current time by executing Date.now(). + + ${instructions} + `; + + return instructions; +} diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 53c36b1..58379dd 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -11,46 +11,10 @@ import { ClientOptions } from 'sendblue'; import SendblueAPI from 'sendblue'; import { codeTool } from './code-tool'; import docsSearchTool from './docs-search-tool'; +import { getInstructions } from './instructions'; import { McpOptions } from './options'; import { blockedMethodsForCodeTool } from './methods'; import { HandlerFunction, McpRequestContext, ToolCallResult, McpTool } from './types'; -import { readEnv } from './util'; - -async function getInstructions(stainlessApiKey: string | undefined): Promise { - // Setting the stainless API key is optional, but may be required - // to authenticate requests to the Stainless API. - const response = await fetch( - readEnv('CODE_MODE_INSTRUCTIONS_URL') ?? 'https://api.stainless.com/api/ai/instructions/sendblue-api', - { - method: 'GET', - headers: { ...(stainlessApiKey && { Authorization: stainlessApiKey }) }, - }, - ); - - let instructions: string | undefined; - if (!response.ok) { - console.warn( - 'Warning: failed to retrieve MCP server instructions. Proceeding with default instructions...', - ); - - instructions = ` - This is the sendblue-api MCP server. You will use Code Mode to help the user perform - actions. You can use search_docs tool to learn about how to take action with this server. Then, - you will write TypeScript code using the execute tool take action. It is CRITICAL that you be - thoughtful and deliberate when executing code. Always try to entirely solve the problem in code - block: it can be as long as you need to get the job done! - `; - } - - instructions ??= ((await response.json()) as { instructions: string }).instructions; - instructions = ` - The current time in Unix timestamps is ${Date.now()}. - - ${instructions} - `; - - return instructions; -} export const newMcpServer = async (stainlessApiKey: string | undefined) => new McpServer( From 0b99f6924a49cdc17d09e5cdac932130f5b22a13 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 06:25:48 +0000 Subject: [PATCH 06/10] fix(mcp): initialize SDK lazily to avoid failing the connection on init errors --- packages/mcp-server/src/http.ts | 31 +++++---------- packages/mcp-server/src/index.ts | 2 +- packages/mcp-server/src/server.ts | 64 ++++++++++++++++++++++++------- 3 files changed, 62 insertions(+), 35 deletions(-) diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index 2a0c705..e2709b2 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -24,28 +24,17 @@ const newServer = async ({ const stainlessApiKey = getStainlessApiKey(req, mcpOptions); const server = await newMcpServer(stainlessApiKey); - try { - const authOptions = parseClientAuthHeaders(req, false); + const authOptions = parseClientAuthHeaders(req, false); - await initMcpServer({ - server: server, - mcpOptions: mcpOptions, - clientOptions: { - ...clientOptions, - ...authOptions, - }, - stainlessApiKey: stainlessApiKey, - }); - } catch (error) { - res.status(401).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: `Unauthorized: ${error instanceof Error ? error.message : error}`, - }, - }); - return null; - } + await initMcpServer({ + server: server, + mcpOptions: mcpOptions, + clientOptions: { + ...clientOptions, + ...authOptions, + }, + stainlessApiKey: stainlessApiKey, + }); return server; }; diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 003a765..654d25c 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -24,7 +24,7 @@ async function main() { await launchStreamableHTTPServer({ mcpOptions: options, debug: options.debug, - port: options.port ?? options.socket, + port: options.socket ?? options.port, }); break; } diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 58379dd..14a1a13 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -55,14 +55,32 @@ export async function initMcpServer(params: { error: logAtLevel('error'), }; - let client = new SendblueAPI({ - logger, - ...params.clientOptions, - defaultHeaders: { - ...params.clientOptions?.defaultHeaders, - 'X-Stainless-MCP': 'true', - }, - }); + let _client: SendblueAPI | undefined; + let _clientError: Error | undefined; + let _logLevel: 'debug' | 'info' | 'warn' | 'error' | 'off' | undefined; + + const getClient = (): SendblueAPI => { + if (_clientError) throw _clientError; + if (!_client) { + try { + _client = new SendblueAPI({ + logger, + ...params.clientOptions, + defaultHeaders: { + ...params.clientOptions?.defaultHeaders, + 'X-Stainless-MCP': 'true', + }, + }); + if (_logLevel) { + _client = _client.withOptions({ logLevel: _logLevel }); + } + } catch (e) { + _clientError = e instanceof Error ? e : new Error(String(e)); + throw _clientError; + } + } + return _client; + }; const providedTools = selectTools(params.mcpOptions); const toolMap = Object.fromEntries(providedTools.map((mcpTool) => [mcpTool.tool.name, mcpTool])); @@ -80,6 +98,21 @@ export async function initMcpServer(params: { throw new Error(`Unknown tool: ${name}`); } + let client: SendblueAPI; + try { + client = getClient(); + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Failed to initialize client: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + return executeHandler({ handler: mcpTool.handler, reqContext: { @@ -92,24 +125,29 @@ export async function initMcpServer(params: { server.setRequestHandler(SetLevelRequestSchema, async (request) => { const { level } = request.params; + let logLevel: 'debug' | 'info' | 'warn' | 'error' | 'off'; switch (level) { case 'debug': - client = client.withOptions({ logLevel: 'debug' }); + logLevel = 'debug'; break; case 'info': - client = client.withOptions({ logLevel: 'info' }); + logLevel = 'info'; break; case 'notice': case 'warning': - client = client.withOptions({ logLevel: 'warn' }); + logLevel = 'warn'; break; case 'error': - client = client.withOptions({ logLevel: 'error' }); + logLevel = 'error'; break; default: - client = client.withOptions({ logLevel: 'off' }); + logLevel = 'off'; break; } + _logLevel = logLevel; + if (_client) { + _client = _client.withOptions({ logLevel }); + } return {}; }); } From f322a40ea28a1b9251b0e296ef7ab92a6c8d7888 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 06:28:43 +0000 Subject: [PATCH 07/10] chore(internal): remove mock server code --- scripts/mock | 41 ----------------------------------------- scripts/test | 46 ---------------------------------------------- 2 files changed, 87 deletions(-) delete mode 100755 scripts/mock diff --git a/scripts/mock b/scripts/mock deleted file mode 100755 index 0b28f6e..0000000 --- a/scripts/mock +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -if [[ -n "$1" && "$1" != '--'* ]]; then - URL="$1" - shift -else - URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" -fi - -# Check if the URL is empty -if [ -z "$URL" ]; then - echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" - exit 1 -fi - -echo "==> Starting mock server with URL ${URL}" - -# Run prism mock on the given spec -if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & - - # Wait for server to come online - echo -n "Waiting for server" - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do - echo -n "." - sleep 0.1 - done - - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - - echo -else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" -fi diff --git a/scripts/test b/scripts/test index 7bce051..548da9b 100755 --- a/scripts/test +++ b/scripts/test @@ -4,53 +4,7 @@ set -e cd "$(dirname "$0")/.." -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' # No Color -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 -} - -kill_server_on_port() { - pids=$(lsof -t -i tcp:"$1" || echo "") - if [ "$pids" != "" ]; then - kill "$pids" - echo "Stopped $pids." - fi -} - -function is_overriding_api_base_url() { - [ -n "$TEST_API_BASE_URL" ] -} - -if ! is_overriding_api_base_url && ! prism_is_running ; then - # When we exit this script, make sure to kill the background mock server process - trap 'kill_server_on_port 4010' EXIT - - # Start the dev server - ./scripts/mock --daemon -fi - -if is_overriding_api_base_url ; then - echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" - echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" - echo -e "running against your OpenAPI spec." - echo - echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" - echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" - echo - - exit 1 -else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" - echo -fi echo "==> Running tests" ./node_modules/.bin/jest "$@" From 32ba29c2431daee857b1a3eddc695e372cab3436 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 06:29:59 +0000 Subject: [PATCH 08/10] chore: update mock server docs --- CONTRIBUTING.md | 6 ------ tests/api-resources/contacts/bulk.test.ts | 8 ++++---- tests/api-resources/contacts/contacts.test.ts | 20 +++++++++---------- tests/api-resources/groups.test.ts | 8 ++++---- tests/api-resources/lookups.test.ts | 4 ++-- tests/api-resources/media-objects.test.ts | 4 ++-- tests/api-resources/messages.test.ts | 14 ++++++------- tests/api-resources/typing-indicators.test.ts | 4 ++-- tests/api-resources/webhooks.test.ts | 14 ++++++------- 9 files changed, 38 insertions(+), 44 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38dc266..6dac594 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,12 +65,6 @@ $ pnpm link -—global sendblue ## Running tests -Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. - -```sh -$ npx prism mock path/to/your/openapi.yml -``` - ```sh $ yarn run test ``` diff --git a/tests/api-resources/contacts/bulk.test.ts b/tests/api-resources/contacts/bulk.test.ts index c6ea79a..24d26db 100644 --- a/tests/api-resources/contacts/bulk.test.ts +++ b/tests/api-resources/contacts/bulk.test.ts @@ -9,7 +9,7 @@ const client = new SendblueAPI({ }); describe('resource bulk', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: only required params', async () => { const responsePromise = client.contacts.bulk.create({ contacts: [{ phone: 'phone' }] }); const rawResponse = await responsePromise.asResponse(); @@ -21,7 +21,7 @@ describe('resource bulk', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: required and optional params', async () => { const response = await client.contacts.bulk.create({ contacts: [ @@ -37,7 +37,7 @@ describe('resource bulk', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete: only required params', async () => { const responsePromise = client.contacts.bulk.delete({ contact_ids: ['+1234567890', '+0987654321'] }); const rawResponse = await responsePromise.asResponse(); @@ -49,7 +49,7 @@ describe('resource bulk', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete: required and optional params', async () => { const response = await client.contacts.bulk.delete({ contact_ids: ['+1234567890', '+0987654321'] }); }); diff --git a/tests/api-resources/contacts/contacts.test.ts b/tests/api-resources/contacts/contacts.test.ts index 67dc6cc..b78eea3 100644 --- a/tests/api-resources/contacts/contacts.test.ts +++ b/tests/api-resources/contacts/contacts.test.ts @@ -9,7 +9,7 @@ const client = new SendblueAPI({ }); describe('resource contacts', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: only required params', async () => { const responsePromise = client.contacts.create({ number: 'number' }); const rawResponse = await responsePromise.asResponse(); @@ -21,7 +21,7 @@ describe('resource contacts', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: required and optional params', async () => { const response = await client.contacts.create({ number: 'number', @@ -41,7 +41,7 @@ describe('resource contacts', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('retrieve', async () => { const responsePromise = client.contacts.retrieve('+1234567890'); const rawResponse = await responsePromise.asResponse(); @@ -53,7 +53,7 @@ describe('resource contacts', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('update', async () => { const responsePromise = client.contacts.update('+1234567890', {}); const rawResponse = await responsePromise.asResponse(); @@ -65,7 +65,7 @@ describe('resource contacts', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('list', async () => { const responsePromise = client.contacts.list(); const rawResponse = await responsePromise.asResponse(); @@ -77,7 +77,7 @@ describe('resource contacts', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('list: request options and params are passed correctly', async () => { // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error await expect( @@ -95,7 +95,7 @@ describe('resource contacts', () => { ).rejects.toThrow(SendblueAPI.NotFoundError); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete', async () => { const responsePromise = client.contacts.delete('+1234567890'); const rawResponse = await responsePromise.asResponse(); @@ -107,7 +107,7 @@ describe('resource contacts', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('count', async () => { const responsePromise = client.contacts.count(); const rawResponse = await responsePromise.asResponse(); @@ -119,7 +119,7 @@ describe('resource contacts', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('verify: only required params', async () => { const responsePromise = client.contacts.verify({ number: 'number' }); const rawResponse = await responsePromise.asResponse(); @@ -131,7 +131,7 @@ describe('resource contacts', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('verify: required and optional params', async () => { const response = await client.contacts.verify({ number: 'number' }); }); diff --git a/tests/api-resources/groups.test.ts b/tests/api-resources/groups.test.ts index 3490307..9194e90 100644 --- a/tests/api-resources/groups.test.ts +++ b/tests/api-resources/groups.test.ts @@ -9,7 +9,7 @@ const client = new SendblueAPI({ }); describe('resource groups', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('modify: only required params', async () => { const responsePromise = client.groups.modify({ group_id: 'group_123456', @@ -25,7 +25,7 @@ describe('resource groups', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('modify: required and optional params', async () => { const response = await client.groups.modify({ group_id: 'group_123456', @@ -34,7 +34,7 @@ describe('resource groups', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('sendMessage: only required params', async () => { const responsePromise = client.groups.sendMessage({ content: 'Hello, everyone!', @@ -49,7 +49,7 @@ describe('resource groups', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('sendMessage: required and optional params', async () => { const response = await client.groups.sendMessage({ content: 'Hello, everyone!', diff --git a/tests/api-resources/lookups.test.ts b/tests/api-resources/lookups.test.ts index 10705e6..d03ba83 100644 --- a/tests/api-resources/lookups.test.ts +++ b/tests/api-resources/lookups.test.ts @@ -9,7 +9,7 @@ const client = new SendblueAPI({ }); describe('resource lookups', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('lookupNumber: only required params', async () => { const responsePromise = client.lookups.lookupNumber({ number: '+19999999999' }); const rawResponse = await responsePromise.asResponse(); @@ -21,7 +21,7 @@ describe('resource lookups', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('lookupNumber: required and optional params', async () => { const response = await client.lookups.lookupNumber({ number: '+19999999999' }); }); diff --git a/tests/api-resources/media-objects.test.ts b/tests/api-resources/media-objects.test.ts index 0ba6e00..97f68c2 100644 --- a/tests/api-resources/media-objects.test.ts +++ b/tests/api-resources/media-objects.test.ts @@ -9,7 +9,7 @@ const client = new SendblueAPI({ }); describe('resource mediaObjects', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('upload: only required params', async () => { const responsePromise = client.mediaObjects.upload({ media_url: 'https://example.com/image.jpg' }); const rawResponse = await responsePromise.asResponse(); @@ -21,7 +21,7 @@ describe('resource mediaObjects', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('upload: required and optional params', async () => { const response = await client.mediaObjects.upload({ media_url: 'https://example.com/image.jpg' }); }); diff --git a/tests/api-resources/messages.test.ts b/tests/api-resources/messages.test.ts index c356867..92a4452 100644 --- a/tests/api-resources/messages.test.ts +++ b/tests/api-resources/messages.test.ts @@ -9,7 +9,7 @@ const client = new SendblueAPI({ }); describe('resource messages', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('retrieve', async () => { const responsePromise = client.messages.retrieve('msg_abc123def456'); const rawResponse = await responsePromise.asResponse(); @@ -21,7 +21,7 @@ describe('resource messages', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('list', async () => { const responsePromise = client.messages.list(); const rawResponse = await responsePromise.asResponse(); @@ -33,7 +33,7 @@ describe('resource messages', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('list: request options and params are passed correctly', async () => { // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error await expect( @@ -66,7 +66,7 @@ describe('resource messages', () => { ).rejects.toThrow(SendblueAPI.NotFoundError); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('getStatus: only required params', async () => { const responsePromise = client.messages.getStatus({ handle: 'msg_abc123def456' }); const rawResponse = await responsePromise.asResponse(); @@ -78,12 +78,12 @@ describe('resource messages', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('getStatus: required and optional params', async () => { const response = await client.messages.getStatus({ handle: 'msg_abc123def456' }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('send: only required params', async () => { const responsePromise = client.messages.send({ content: 'Hello, World!', @@ -99,7 +99,7 @@ describe('resource messages', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('send: required and optional params', async () => { const response = await client.messages.send({ content: 'Hello, World!', diff --git a/tests/api-resources/typing-indicators.test.ts b/tests/api-resources/typing-indicators.test.ts index 8af1182..8cb4e78 100644 --- a/tests/api-resources/typing-indicators.test.ts +++ b/tests/api-resources/typing-indicators.test.ts @@ -9,7 +9,7 @@ const client = new SendblueAPI({ }); describe('resource typingIndicators', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('send: only required params', async () => { const responsePromise = client.typingIndicators.send({ from_number: '+16292925296', @@ -24,7 +24,7 @@ describe('resource typingIndicators', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('send: required and optional params', async () => { const response = await client.typingIndicators.send({ from_number: '+16292925296', diff --git a/tests/api-resources/webhooks.test.ts b/tests/api-resources/webhooks.test.ts index c666a0d..e0b127b 100644 --- a/tests/api-resources/webhooks.test.ts +++ b/tests/api-resources/webhooks.test.ts @@ -9,7 +9,7 @@ const client = new SendblueAPI({ }); describe('resource webhooks', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: only required params', async () => { const responsePromise = client.webhooks.create({ webhooks: ['https://example.com'] }); const rawResponse = await responsePromise.asResponse(); @@ -21,7 +21,7 @@ describe('resource webhooks', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: required and optional params', async () => { const response = await client.webhooks.create({ webhooks: ['https://example.com'], @@ -30,7 +30,7 @@ describe('resource webhooks', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('update: only required params', async () => { const responsePromise = client.webhooks.update({ webhooks: {} }); const rawResponse = await responsePromise.asResponse(); @@ -42,7 +42,7 @@ describe('resource webhooks', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('update: required and optional params', async () => { const response = await client.webhooks.update({ webhooks: { @@ -58,7 +58,7 @@ describe('resource webhooks', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('list', async () => { const responsePromise = client.webhooks.list(); const rawResponse = await responsePromise.asResponse(); @@ -70,7 +70,7 @@ describe('resource webhooks', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete: only required params', async () => { const responsePromise = client.webhooks.delete({ webhooks: ['https://example.com'] }); const rawResponse = await responsePromise.asResponse(); @@ -82,7 +82,7 @@ describe('resource webhooks', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete: required and optional params', async () => { const response = await client.webhooks.delete({ webhooks: ['https://example.com'], type: 'receive' }); }); From 45a3ed52cc155a8be91982997764f5dda2987052 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 06:39:55 +0000 Subject: [PATCH 09/10] chore(mcp): correctly update version in sync with sdk --- release-please-config.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/release-please-config.json b/release-please-config.json index b190980..9b04279 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -68,6 +68,11 @@ "type": "json", "path": "packages/mcp-server/package.json", "jsonpath": "$.version" + }, + { + "type": "json", + "path": "packages/mcp-server/manifest.json", + "jsonpath": "$.version" } ] } From 60de51dc622bcd0a90fd2f4d16be5c962b2ebb97 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 06:40:14 +0000 Subject: [PATCH 10/10] release: 3.1.2 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 20 ++++++++++++++++++++ package.json | 2 +- packages/mcp-server/manifest.json | 10 +++++++--- packages/mcp-server/package.json | 2 +- packages/mcp-server/src/server.ts | 2 +- src/version.ts | 2 +- 7 files changed, 32 insertions(+), 8 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b4b8d0f..50d9d3b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.1.1" + ".": "3.1.2" } diff --git a/CHANGELOG.md b/CHANGELOG.md index d2bf9c0..1953329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 3.1.2 (2026-02-20) + +Full Changelog: [v3.1.1...v3.1.2](https://github.com/sendblue-api/sendblue-ts/compare/v3.1.1...v3.1.2) + +### Bug Fixes + +* **mcp:** initialize SDK lazily to avoid failing the connection on init errors ([0b99f69](https://github.com/sendblue-api/sendblue-ts/commit/0b99f6924a49cdc17d09e5cdac932130f5b22a13)) + + +### Chores + +* **internal/client:** fix form-urlencoded requests ([0636408](https://github.com/sendblue-api/sendblue-ts/commit/06364085dd59f4b43b2f0f95b8bab83435fa9ffb)) +* **internal:** allow setting x-stainless-api-key header on mcp server requests ([cdbf955](https://github.com/sendblue-api/sendblue-ts/commit/cdbf955893b151aa89778d292b3e041a476e0306)) +* **internal:** cache fetch instruction calls in MCP server ([3c55910](https://github.com/sendblue-api/sendblue-ts/commit/3c5591018ee53d8e51d94100f6cafa0c02572046)) +* **internal:** improve layout of generated MCP server files ([01a4563](https://github.com/sendblue-api/sendblue-ts/commit/01a45632c1ee8e9eec2a3995a1b7891059d1b101)) +* **internal:** remove mock server code ([f322a40](https://github.com/sendblue-api/sendblue-ts/commit/f322a40ea28a1b9251b0e296ef7ab92a6c8d7888)) +* **mcp:** correctly update version in sync with sdk ([45a3ed5](https://github.com/sendblue-api/sendblue-ts/commit/45a3ed52cc155a8be91982997764f5dda2987052)) +* **mcp:** forward STAINLESS_API_KEY to docs search endpoint ([869a8aa](https://github.com/sendblue-api/sendblue-ts/commit/869a8aa557dfae546e648a5d92e86432703c2cff)) +* update mock server docs ([32ba29c](https://github.com/sendblue-api/sendblue-ts/commit/32ba29c2431daee857b1a3eddc695e372cab3436)) + ## 3.1.1 (2026-02-12) Full Changelog: [v3.1.0...v3.1.1](https://github.com/sendblue-api/sendblue-ts/compare/v3.1.0...v3.1.1) diff --git a/package.json b/package.json index 749312f..fe8f2dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sendblue", - "version": "3.1.1", + "version": "3.1.2", "description": "The official TypeScript library for the Sendblue API API", "author": "Sendblue API ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/manifest.json b/packages/mcp-server/manifest.json index 8c0b431..a7bfac2 100644 --- a/packages/mcp-server/manifest.json +++ b/packages/mcp-server/manifest.json @@ -1,7 +1,7 @@ { "dxt_version": "0.2", "name": "sendblue-mcp", - "version": "2.0.1", + "version": "3.1.2", "description": "The official MCP Server for the Sendblue API API", "author": { "name": "Sendblue API", @@ -18,7 +18,9 @@ "entry_point": "index.js", "mcp_config": { "command": "node", - "args": ["${__dirname}/index.js"], + "args": [ + "${__dirname}/index.js" + ], "env": { "SENDBLUE_API_API_KEY": "${user_config.SENDBLUE_API_API_KEY}", "SENDBLUE_API_API_SECRET": "${user_config.SENDBLUE_API_API_SECRET}" @@ -46,5 +48,7 @@ "node": ">=18.0.0" } }, - "keywords": ["api"] + "keywords": [ + "api" + ] } diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 5c9e84d..9a093e8 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "sendblue-mcp", - "version": "3.1.1", + "version": "3.1.2", "description": "The official MCP Server for the Sendblue API API", "author": "Sendblue API ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 14a1a13..1f460e6 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -20,7 +20,7 @@ export const newMcpServer = async (stainlessApiKey: string | undefined) => new McpServer( { name: 'sendblue_api', - version: '3.1.1', + version: '3.1.2', }, { instructions: await getInstructions(stainlessApiKey), diff --git a/src/version.ts b/src/version.ts index 6883c1e..0716f80 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '3.1.1'; // x-release-please-version +export const VERSION = '3.1.2'; // x-release-please-version