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

## 1.7.2 (2026-02-18)

Full Changelog: [v1.7.1...v1.7.2](https://github.com/CASParser/cas-parser-node/compare/v1.7.1...v1.7.2)

### Chores

* **internal/client:** fix form-urlencoded requests ([f39cb7f](https://github.com/CASParser/cas-parser-node/commit/f39cb7f6c2ad7c20da495ded91c644e432596425))
* **internal:** allow setting x-stainless-api-key header on mcp server requests ([5b3baf7](https://github.com/CASParser/cas-parser-node/commit/5b3baf7ead67ae4a2e371ceda87d5d9e97019c5d))

## 1.7.1 (2026-02-14)

Full Changelog: [v1.7.0...v1.7.1](https://github.com/CASParser/cas-parser-node/compare/v1.7.0...v1.7.1)
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": "cas-parser-node",
"version": "1.7.1",
"version": "1.7.2",
"description": "The official TypeScript library for the Cas Parser API",
"author": "Cas Parser <sameer@casparser.in>",
"types": "dist/index.d.ts",
Expand Down
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": "cas-parser-node-mcp",
"version": "1.7.1",
"version": "1.7.2",
"description": "The official MCP Server for the Cas Parser API",
"author": "Cas Parser <sameer@casparser.in>",
"types": "dist/index.d.ts",
Expand Down
17 changes: 16 additions & 1 deletion packages/mcp-server/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,24 @@

import { IncomingMessage } from 'node:http';
import { ClientOptions } from 'cas-parser-node';
import { McpOptions } from './options';

export const parseAuthHeaders = (req: IncomingMessage, required?: boolean): Partial<ClientOptions> => {
export const parseClientAuthHeaders = (req: IncomingMessage, required?: boolean): Partial<ClientOptions> => {
const apiKey =
Array.isArray(req.headers['x-api-key']) ? req.headers['x-api-key'][0] : req.headers['x-api-key'];
return { apiKey };
};

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;
};
33 changes: 21 additions & 12 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 './util';
import { WorkerInput, WorkerOutput } from './code-tool-types';
import { SdkMethod } from './methods';
import { CasParser } from 'cas-parser-node';

const prompt = `Runs JavaScript code to interact with the Cas Parser API.

Expand Down Expand Up @@ -36,7 +42,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 @@ -56,19 +62,24 @@ export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): M
required: ['code'],
},
};
const handler = async (client: CasParser, 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 @@ -78,16 +89,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({
CAS_PARSER_API_KEY: requireValue(
Expand Down
15 changes: 9 additions & 6 deletions packages/mcp-server/src/docs-search-tool.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -43,13 +41,18 @@ export const tool: Tool = {
const docsSearchURL =
process.env['DOCS_SEARCH_URL'] || 'https://api.stainless.com/api/projects/cas-parser/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 stainlessAPIKey = readEnv('STAINLESS_API_KEY');
const result = await fetch(`${docsSearchURL}?${query}`, {
headers: {
...(stainlessAPIKey && { Authorization: stainlessAPIKey }),
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
},
});

Expand Down
21 changes: 14 additions & 7 deletions packages/mcp-server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ClientOptions } from 'cas-parser-node';
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';

Expand All @@ -21,17 +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);
const authOptions = parseClientAuthHeaders(req, false);

await initMcpServer({
server: server,
mcpOptions: mcpOptions,
clientOptions: {
...clientOptions,
...authOptions,
},
stainlessApiKey: stainlessApiKey,
});
} catch (error) {
res.status(401).json({
Expand Down Expand Up @@ -112,20 +115,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}`);
}
};
9 changes: 9 additions & 0 deletions packages/mcp-server/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
42 changes: 27 additions & 15 deletions packages/mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
// 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/cas-parser',
{
method: 'GET',
headers: { ...(stainlessAPIKey && { Authorization: stainlessAPIKey }) },
headers: { ...(stainlessApiKey && { Authorization: stainlessApiKey }) },
},
);

Expand Down Expand Up @@ -52,14 +52,14 @@ async function getInstructions() {
return instructions;
}

export const newMcpServer = async () =>
export const newMcpServer = async (stainlessApiKey: string | undefined) =>
new McpServer(
{
name: 'cas_parser_node_api',
version: '1.7.1',
version: '1.7.2',
},
{
instructions: await getInstructions(),
instructions: await getInstructions(stainlessApiKey),
capabilities: { tools: {}, logging: {} },
},
);
Expand All @@ -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;

Expand Down Expand Up @@ -116,7 +117,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) => {
Expand Down Expand Up @@ -161,10 +169,14 @@ export function selectTools(options?: McpOptions): McpTool[] {
/**
* Runs the provided handler with the given client and arguments.
*/
export async function executeHandler(
handler: HandlerFunction,
client: CasParser,
args: Record<string, unknown> | undefined,
) {
return await handler(client, args || {});
export async function executeHandler({
handler,
reqContext,
args,
}: {
handler: HandlerFunction;
reqContext: McpRequestContext;
args: Record<string, unknown> | undefined;
}): Promise<ToolCallResult> {
return await handler({ reqContext, args: args || {} });
}
4 changes: 2 additions & 2 deletions packages/mcp-server/src/stdio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 12 additions & 4 deletions packages/mcp-server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,18 @@ export type ToolCallResult = {
isError?: boolean;
};

export type HandlerFunction = (
client: CasParser,
args: Record<string, unknown> | undefined,
) => Promise<ToolCallResult>;
export type McpRequestContext = {
client: CasParser;
stainlessApiKey?: string | undefined;
};

export type HandlerFunction = ({
reqContext,
args,
}: {
reqContext: McpRequestContext;
args: Record<string, unknown> | undefined;
}) => Promise<ToolCallResult>;

export function asTextContentResult(result: unknown): ToolCallResult {
return {
Expand Down
Loading