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 @@
{
".": "0.21.0"
".": "0.21.1"
}
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 0.21.1 (2026-03-04)

Full Changelog: [v0.21.0...v0.21.1](https://github.com/isaacus-dev/isaacus-typescript/compare/v0.21.0...v0.21.1)

### Chores

* **internal:** codegen related update ([61d1a70](https://github.com/isaacus-dev/isaacus-typescript/commit/61d1a70e6af2ff3c1c0f16517bca200f162cf9c1))
* **internal:** use x-stainless-mcp-client-envs header for MCP remote code tool calls ([e372066](https://github.com/isaacus-dev/isaacus-typescript/commit/e372066ae02d0ffcf5fae210ba027c9b13950141))
* **mcp-server:** return access instructions for 404 without API key ([8d188e6](https://github.com/isaacus-dev/isaacus-typescript/commit/8d188e6a165648c20580f15c5f4ce8febc7acab1))

## 0.21.0 (2026-03-03)

Full Changelog: [v0.20.2...v0.21.0](https://github.com/isaacus-dev/isaacus-typescript/compare/v0.20.2...v0.21.0)
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": "isaacus",
"version": "0.21.0",
"version": "0.21.1",
"description": "The official TypeScript library for the Isaacus API",
"author": "Isaacus <support@isaacus.com>",
"types": "dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"dxt_version": "0.2",
"name": "isaacus-mcp",
"version": "0.21.0",
"version": "0.21.1",
"description": "The official MCP Server for the Isaacus API",
"author": {
"name": "Isaacus",
Expand Down
8 changes: 4 additions & 4 deletions packages/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "isaacus-mcp",
"version": "0.21.0",
"version": "0.21.1",
"description": "The official MCP Server for the Isaacus API",
"author": "Isaacus <support@isaacus.com>",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -39,8 +39,9 @@
"express": "^5.1.0",
"fuse.js": "^7.1.0",
"jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz",
"morgan": "^1.10.0",
"morgan-body": "^2.6.9",
"pino": "^10.3.1",
"pino-http": "^11.0.0",
"pino-pretty": "^13.1.3",
"qs": "^6.14.1",
"typescript": "5.8.3",
"yargs": "^17.7.2",
Expand All @@ -57,7 +58,6 @@
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"@types/jest": "^29.4.0",
"@types/morgan": "^1.9.10",
"@types/qs": "^6.14.0",
"@types/yargs": "^17.0.8",
"@typescript-eslint/eslint-plugin": "8.31.1",
Expand Down
30 changes: 27 additions & 3 deletions packages/mcp-server/src/code-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { readEnv, requireValue } from './util';
import { WorkerInput, WorkerOutput } from './code-tool-types';
import { getLogger } from './logger';
import { SdkMethod } from './methods';
import { McpCodeExecutionMode } from './options';
import { ClientOptions } from 'isaacus';
Expand Down Expand Up @@ -87,6 +88,8 @@ export function codeTool({
},
};

const logger = getLogger();

const handler = async ({
reqContext,
args,
Expand All @@ -111,11 +114,27 @@ export function codeTool({
}
}

let result: ToolCallResult;
const startTime = Date.now();

if (codeExecutionMode === 'local') {
return await localDenoHandler({ reqContext, args });
logger.debug('Executing code in local Deno environment');
result = await localDenoHandler({ reqContext, args });
} else {
return await remoteStainlessHandler({ reqContext, args });
logger.debug('Executing code in remote Stainless environment');
result = await remoteStainlessHandler({ reqContext, args });
}

logger.info(
{
codeExecutionMode,
durationMs: Date.now() - startTime,
isError: result.isError,
contentRows: result.content?.length ?? 0,
},
'Got code tool execution result',
);
return result;
};

return { metadata, tool, handler };
Expand All @@ -140,7 +159,7 @@ const remoteStainlessHandler = async ({
headers: {
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
'Content-Type': 'application/json',
client_envs: JSON.stringify({
'x-stainless-mcp-client-envs': JSON.stringify({
ISAACUS_API_KEY: requireValue(
readEnv('ISAACUS_API_KEY') ?? client.apiKey,
'set ISAACUS_API_KEY environment variable or provide apiKey client option',
Expand All @@ -157,6 +176,11 @@ const remoteStainlessHandler = async ({
});

if (!res.ok) {
if (res.status === 404 && !reqContext.stainlessApiKey) {
throw new Error(
'Could not access code tool for this project. You may need to provide a Stainless API key via the STAINLESS_API_KEY environment variable, the --stainless-api-key flag, or the x-stainless-api-key HTTP header.',
);
}
throw new Error(
`${res.status}: ${
res.statusText
Expand Down
37 changes: 34 additions & 3 deletions packages/mcp-server/src/docs-search-tool.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

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

export const metadata: Metadata = {
resource: 'all',
Expand Down Expand Up @@ -50,19 +51,49 @@ export const handler = async ({
}) => {
const body = args as any;
const query = new URLSearchParams(body).toString();

const startTime = Date.now();
const result = await fetch(`${docsSearchURL}?${query}`, {
headers: {
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
},
});

const logger = getLogger();

if (!result.ok) {
const errorText = await result.text();
logger.warn(
{
durationMs: Date.now() - startTime,
query: body.query,
status: result.status,
statusText: result.statusText,
errorText,
},
'Got error response from docs search tool',
);

if (result.status === 404 && !reqContext.stainlessApiKey) {
throw new Error(
'Could not find docs for this project. You may need to provide a Stainless API key via the STAINLESS_API_KEY environment variable, the --stainless-api-key flag, or the x-stainless-api-key HTTP header.',
);
}

throw new Error(
`${result.status}: ${result.statusText} when using doc search tool. Details: ${await result.text()}`,
`${result.status}: ${result.statusText} when using doc search tool. Details: ${errorText}`,
);
}

return asTextContentResult(await result.json());
const resultBody = await result.json();
logger.info(
{
durationMs: Date.now() - startTime,
query: body.query,
},
'Got docs search result',
);
return asTextContentResult(resultBody);
};

export default { metadata, tool, handler };
74 changes: 53 additions & 21 deletions packages/mcp-server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { ClientOptions } from 'isaacus';
import express from 'express';
import morgan from 'morgan';
import morganBody from 'morgan-body';
import pino from 'pino';
import pinoHttp from 'pino-http';
import { getStainlessApiKey, parseClientAuthHeaders } from './auth';
import { getLogger } from './logger';
import { McpOptions } from './options';
import { initMcpServer, newMcpServer } from './server';

Expand Down Expand Up @@ -70,29 +71,60 @@ const del = async (req: express.Request, res: express.Response) => {
});
};

const redactHeaders = (headers: Record<string, any>) => {
const hiddenHeaders = /auth|cookie|key|token/i;
const filtered = { ...headers };
Object.keys(filtered).forEach((key) => {
if (hiddenHeaders.test(key)) {
filtered[key] = '[REDACTED]';
}
});
return filtered;
};

export const streamableHTTPApp = ({
clientOptions = {},
mcpOptions,
debug,
}: {
clientOptions?: ClientOptions;
mcpOptions: McpOptions;
debug: boolean;
}): express.Express => {
const app = express();
app.set('query parser', 'extended');
app.use(express.json());

if (debug) {
morganBody(app, {
logAllReqHeader: true,
logAllResHeader: true,
logRequestBody: true,
logResponseBody: true,
});
} else {
app.use(morgan('combined'));
}
app.use(
pinoHttp({
logger: getLogger(),
customLogLevel: (req, res) => {
if (res.statusCode >= 500) {
return 'error';
} else if (res.statusCode >= 400) {
return 'warn';
}
return 'info';
},
customSuccessMessage: function (req, res) {
return `Request ${req.method} to ${req.url} completed with status ${res.statusCode}`;
},
customErrorMessage: function (req, res, err) {
return `Request ${req.method} to ${req.url} errored with status ${res.statusCode}`;
},
serializers: {
req: pino.stdSerializers.wrapRequestSerializer((req) => {
return {
...req,
headers: redactHeaders(req.raw.headers),
};
}),
res: pino.stdSerializers.wrapResponseSerializer((res) => {
return {
...res,
headers: redactHeaders(res.headers),
};
}),
},
}),
);

app.get('/health', async (req: express.Request, res: express.Response) => {
res.status(200).send('OK');
Expand All @@ -106,22 +138,22 @@ export const streamableHTTPApp = ({

export const launchStreamableHTTPServer = async ({
mcpOptions,
debug,
port,
}: {
mcpOptions: McpOptions;
debug: boolean;
port: number | string | undefined;
}) => {
const app = streamableHTTPApp({ mcpOptions, debug });
const app = streamableHTTPApp({ mcpOptions });
const server = app.listen(port);
const address = server.address();

const logger = getLogger();

if (typeof address === 'string') {
console.error(`MCP Server running on streamable HTTP at ${address}`);
logger.info(`MCP Server running on streamable HTTP at ${address}`);
} else if (address !== null) {
console.error(`MCP Server running on streamable HTTP on port ${address.port}`);
logger.info(`MCP Server running on streamable HTTP on port ${address.port}`);
} else {
console.error(`MCP Server running on streamable HTTP on port ${port}`);
logger.info(`MCP Server running on streamable HTTP on port ${port}`);
}
};
26 changes: 14 additions & 12 deletions packages/mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@ import { McpOptions, parseCLIOptions } from './options';
import { launchStdioServer } from './stdio';
import { launchStreamableHTTPServer } from './http';
import type { McpTool } from './types';
import { configureLogger, getLogger } from './logger';

async function main() {
const options = parseOptionsOrError();
configureLogger({
level: options.debug ? 'debug' : 'info',
pretty: options.logFormat === 'pretty',
});

const selectedTools = await selectToolsOrError(options);

console.error(
`MCP Server starting with ${selectedTools.length} tools:`,
selectedTools.map((e) => e.tool.name),
getLogger().info(
{ tools: selectedTools.map((e) => e.tool.name) },
`MCP Server starting with ${selectedTools.length} tools`,
);

switch (options.transport) {
Expand All @@ -23,7 +28,6 @@ async function main() {
case 'http':
await launchStreamableHTTPServer({
mcpOptions: options,
debug: options.debug,
port: options.socket ?? options.port,
});
break;
Expand All @@ -32,7 +36,8 @@ async function main() {

if (require.main === module) {
main().catch((error) => {
console.error('Fatal error in main():', error);
// Logger might not be initialized yet
console.error('Fatal error in main()', error);
process.exit(1);
});
}
Expand All @@ -41,7 +46,8 @@ function parseOptionsOrError() {
try {
return parseCLIOptions();
} catch (error) {
console.error('Error parsing options:', error);
// Logger is initialized after options, so use console.error here
console.error('Error parsing options', error);
process.exit(1);
}
}
Expand All @@ -50,16 +56,12 @@ async function selectToolsOrError(options: McpOptions): Promise<McpTool[]> {
try {
const includedTools = selectTools(options);
if (includedTools.length === 0) {
console.error('No tools match the provided filters.');
getLogger().error('No tools match the provided filters');
process.exit(1);
}
return includedTools;
} catch (error) {
if (error instanceof Error) {
console.error('Error filtering tools:', error.message);
} else {
console.error('Error filtering tools:', error);
}
getLogger().error({ error }, 'Error filtering tools');
process.exit(1);
}
}
3 changes: 2 additions & 1 deletion packages/mcp-server/src/instructions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

import { readEnv } from './util';
import { getLogger } from './logger';

const INSTRUCTIONS_CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes

Expand Down Expand Up @@ -50,7 +51,7 @@ async function fetchLatestInstructions(stainlessApiKey: string | undefined): Pro

let instructions: string | undefined;
if (!response.ok) {
console.warn(
getLogger().warn(
'Warning: failed to retrieve MCP server instructions. Proceeding with default instructions...',
);

Expand Down
Loading