Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "3.1.1"
".": "3.1.2"
}
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
6 changes: 0 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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 <support@sendblue.co>",
"types": "dist/index.d.ts",
Expand Down
10 changes: 7 additions & 3 deletions packages/mcp-server/manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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}"
Expand Down Expand Up @@ -46,5 +48,7 @@
"node": ">=18.0.0"
}
},
"keywords": ["api"]
"keywords": [
"api"
]
}
2 changes: 1 addition & 1 deletion packages/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -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 <support@sendblue.co>",
"types": "dist/index.d.ts",
Expand Down
31 changes: 31 additions & 0 deletions packages/mcp-server/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

import { IncomingMessage } from 'node:http';
import { ClientOptions } from 'sendblue';
import { McpOptions } from './options';

export const parseClientAuthHeaders = (req: IncomingMessage, required?: boolean): Partial<ClientOptions> => {
const apiKey =
Array.isArray(req.headers['sb-api-key-id']) ?
req.headers['sb-api-key-id'][0]
: req.headers['sb-api-key-id'];
const apiSecret =
Array.isArray(req.headers['sb-api-secret-key']) ?
req.headers['sb-api-secret-key'][0]
: 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;
};
35 changes: 22 additions & 13 deletions packages/mcp-server/src/code-tool.ts
Original file line number Diff line number Diff line change
@@ -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 './server';
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.

Expand Down Expand Up @@ -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',
Expand All @@ -60,19 +66,24 @@ export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): M
required: ['code'],
},
};
const handler = async (client: SendblueAPI, args: any): Promise<ToolCallResult> => {
const handler = async ({
reqContext,
args,
}: {
reqContext: McpRequestContext;
args: any;
}): Promise<ToolCallResult> => {
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
Expand All @@ -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(
Expand Down
17 changes: 13 additions & 4 deletions packages/mcp-server/src/docs-search-tool.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

import { Metadata, asTextContentResult } from './types';

import { Metadata, McpRequestContext, asTextContentResult } from './types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';

export const metadata: Metadata = {
Expand Down Expand Up @@ -42,10 +41,20 @@ 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<string, unknown> | undefined) => {
export const handler = async ({
reqContext,
args,
}: {
reqContext: McpRequestContext;
args: Record<string, unknown> | undefined;
}) => {
const body = args as any;
const query = new URLSearchParams(body).toString();
const result = await fetch(`${docsSearchURL}?${query}`);
const result = await fetch(`${docsSearchURL}?${query}`, {
headers: {
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
},
});

if (!result.ok) {
throw new Error(
Expand Down
16 changes: 0 additions & 16 deletions packages/mcp-server/src/headers.ts

This file was deleted.

51 changes: 24 additions & 27 deletions packages/mcp-server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { getStainlessApiKey, parseClientAuthHeaders } 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,
Expand All @@ -20,28 +21,20 @@ const newServer = async ({
req: express.Request;
res: express.Response;
}): Promise<McpServer | null> => {
const server = await newMcpServer();
const stainlessApiKey = getStainlessApiKey(req, mcpOptions);
const server = await newMcpServer(stainlessApiKey);

try {
const authOptions = parseAuthHeaders(req, false);
await initMcpServer({
server: server,
mcpOptions: mcpOptions,
clientOptions: {
...clientOptions,
...authOptions,
},
});
} catch (error) {
res.status(401).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: `Unauthorized: ${error instanceof Error ? error.message : error}`,
},
});
return null;
}
const authOptions = parseClientAuthHeaders(req, false);

await initMcpServer({
server: server,
mcpOptions: mcpOptions,
clientOptions: {
...clientOptions,
...authOptions,
},
stainlessApiKey: stainlessApiKey,
});

return server;
};
Expand Down Expand Up @@ -111,20 +104,24 @@ 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') {
console.error(`MCP Server running on streamable HTTP at ${address}`);
} 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}`);
}
};
2 changes: 1 addition & 1 deletion packages/mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading