Skip to content
Merged
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
12 changes: 9 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,28 @@ jobs:
run: ./scripts/build

- name: Get GitHub OIDC Token
if: github.repository == 'stainless-sdks/scan-documents-typescript'
if: |-
github.repository == 'stainless-sdks/scan-documents-typescript' &&
!startsWith(github.ref, 'refs/heads/stl/')
id: github-oidc
uses: actions/github-script@v8
with:
script: core.setOutput('github_token', await core.getIDToken());

- name: Upload tarball
if: github.repository == 'stainless-sdks/scan-documents-typescript'
if: |-
github.repository == 'stainless-sdks/scan-documents-typescript' &&
!startsWith(github.ref, 'refs/heads/stl/')
env:
URL: https://pkg.stainless.com/s
AUTH: ${{ steps.github-oidc.outputs.github_token }}
SHA: ${{ github.sha }}
run: ./scripts/utils/upload-artifact.sh

- name: Upload MCP Server tarball
if: github.repository == 'stainless-sdks/scan-documents-typescript'
if: |-
github.repository == 'stainless-sdks/scan-documents-typescript' &&
!startsWith(github.ref, 'refs/heads/stl/')
env:
URL: https://pkg.stainless.com/s?subpackage=mcp-server
AUTH: ${{ steps.github-oidc.outputs.github_token }}
Expand Down
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.1.0-alpha.29"
".": "0.1.0-alpha.30"
}
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## 0.1.0-alpha.30 (2026-03-08)

Full Changelog: [v0.1.0-alpha.29...v0.1.0-alpha.30](https://github.com/Scan-Documents/node-sdk/compare/v0.1.0-alpha.29...v0.1.0-alpha.30)

### Bug Fixes

* **client:** preserve URL params already embedded in path ([6cbd733](https://github.com/Scan-Documents/node-sdk/commit/6cbd7337b0f0f2e187fc8123c7eef86704327372))


### Chores

* **ci:** skip uploading artifacts on stainless-internal branches ([2aff58a](https://github.com/Scan-Documents/node-sdk/commit/2aff58a69e6caba41e7b9171781a53254f570fc5))
* **internal:** codegen related update ([df64e3c](https://github.com/Scan-Documents/node-sdk/commit/df64e3c1d77cba0a7a3e43d154bd4ba0db69edc0))
* **internal:** codegen related update ([5982213](https://github.com/Scan-Documents/node-sdk/commit/59822139383e80e2b5351c014ab9ced47cce5b6e))
* **internal:** use x-stainless-mcp-client-envs header for MCP remote code tool calls ([807bfb7](https://github.com/Scan-Documents/node-sdk/commit/807bfb793c7ff25838c7b585b93494dcf2050b5a))
* **mcp-server:** return access instructions for 404 without API key ([368d6f4](https://github.com/Scan-Documents/node-sdk/commit/368d6f4e8ace2d9bda932ae9307b827ef7741236))
* update placeholder string ([28577d0](https://github.com/Scan-Documents/node-sdk/commit/28577d087ff00807ee92cb7a9ac36fc670b0f52e))

## 0.1.0-alpha.29 (2026-02-27)

Full Changelog: [v0.1.0-alpha.28...v0.1.0-alpha.29](https://github.com/Scan-Documents/node-sdk/compare/v0.1.0-alpha.28...v0.1.0-alpha.29)
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": "scan-documents",
"version": "0.1.0-alpha.29",
"version": "0.1.0-alpha.30",
"description": "The official TypeScript library for the Scan Documents API",
"author": "Scan Documents <support@scan-documents.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": "scan-documents-mcp",
"version": "0.1.0-alpha.29",
"version": "0.1.0-alpha.30",
"description": "The official MCP Server for the Scan Documents API",
"author": {
"name": "Scan Documents",
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": "scan-documents-mcp",
"version": "0.1.0-alpha.29",
"version": "0.1.0-alpha.30",
"description": "The official MCP Server for the Scan Documents API",
"author": "Scan Documents <support@scan-documents.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 'scan-documents';
Expand Down Expand Up @@ -81,6 +82,8 @@ export function codeTool({
},
};

const logger = getLogger();

const handler = async ({
reqContext,
args,
Expand All @@ -105,11 +108,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 @@ -134,7 +153,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({
SCAN_DOCUMENTS_API_KEY: requireValue(
readEnv('SCAN_DOCUMENTS_API_KEY') ?? client.apiKey,
'set SCAN_DOCUMENTS_API_KEY environment variable or provide apiKey client option',
Expand All @@ -151,6 +170,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 'scan-documents';
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}`);
}
};
Loading