Skip to content

Commit 98e619a

Browse files
feat(mcp): remote server with passthru auth
1 parent 782e9fa commit 98e619a

File tree

7 files changed

+167
-19
lines changed

7 files changed

+167
-19
lines changed

packages/mcp-server/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"dependencies": {
3030
"isaacus": "file:../../dist/",
3131
"@modelcontextprotocol/sdk": "^1.11.5",
32+
"express": "^5.1.0",
3233
"jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.2/jq-web.tar.gz",
3334
"yargs": "^17.7.2",
3435
"@cloudflare/cabidela": "^0.2.4",
@@ -40,6 +41,7 @@
4041
},
4142
"devDependencies": {
4243
"@types/jest": "^29.4.0",
44+
"@types/express": "^5.0.3",
4345
"@typescript-eslint/eslint-plugin": "8.31.1",
4446
"@typescript-eslint/parser": "8.31.1",
4547
"eslint": "^8.49.0",

packages/mcp-server/src/headers.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
3+
import { type ClientOptions } from 'isaacus/client';
4+
5+
import { IncomingMessage } from 'node:http';
6+
7+
export const parseAuthHeaders = (req: IncomingMessage): Partial<ClientOptions> => {
8+
if (req.headers.authorization) {
9+
const scheme = req.headers.authorization.slice(req.headers.authorization.search(' '));
10+
const value = req.headers.authorization.slice(scheme.length + 1);
11+
switch (scheme) {
12+
case 'Bearer':
13+
return { apiKey: req.headers.authorization.slice('Bearer '.length) };
14+
default:
15+
throw new Error(`Unsupported authorization scheme`);
16+
}
17+
}
18+
19+
const apiKey =
20+
req.headers['x-isaacus-api-key'] instanceof Array ?
21+
req.headers['x-isaacus-api-key'][0]
22+
: req.headers['x-isaacus-api-key'];
23+
return { apiKey };
24+
};

packages/mcp-server/src/http.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
2+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3+
4+
import express from 'express';
5+
import { McpOptions } from './options';
6+
import { initMcpServer, newMcpServer } from './server';
7+
import { parseAuthHeaders } from './headers';
8+
import { Endpoint } from './tools';
9+
10+
const newServer = (mcpOptions: McpOptions, req: express.Request, res: express.Response): McpServer | null => {
11+
const server = newMcpServer();
12+
try {
13+
const authOptions = parseAuthHeaders(req);
14+
initMcpServer({
15+
server: server,
16+
clientOptions: {
17+
...authOptions,
18+
defaultHeaders: {
19+
'X-Stainless-MCP': 'true',
20+
},
21+
},
22+
mcpOptions,
23+
});
24+
} catch {
25+
res.status(401).json({
26+
jsonrpc: '2.0',
27+
error: {
28+
code: -32000,
29+
message: 'Unauthorized',
30+
},
31+
});
32+
return null;
33+
}
34+
35+
return server;
36+
};
37+
38+
const post = (defaultOptions: McpOptions) => async (req: express.Request, res: express.Response) => {
39+
const server = newServer(defaultOptions, req, res);
40+
// If we return null, we already set the authorization error.
41+
if (server === null) return;
42+
const transport = new StreamableHTTPServerTransport({
43+
// Stateless server
44+
sessionIdGenerator: undefined,
45+
});
46+
await server.connect(transport);
47+
await transport.handleRequest(req, res, req.body);
48+
};
49+
50+
const get = async (req: express.Request, res: express.Response) => {
51+
res.status(405).json({
52+
jsonrpc: '2.0',
53+
error: {
54+
code: -32000,
55+
message: 'Method not supported',
56+
},
57+
});
58+
};
59+
60+
const del = async (req: express.Request, res: express.Response) => {
61+
res.status(405).json({
62+
jsonrpc: '2.0',
63+
error: {
64+
code: -32000,
65+
message: 'Method not supported',
66+
},
67+
});
68+
};
69+
70+
export const launchStreamableHTTPServer = async (
71+
options: McpOptions,
72+
endpoints: Endpoint[],
73+
port: number | undefined,
74+
) => {
75+
const app = express();
76+
app.use(express.json());
77+
78+
app.get('/', get);
79+
app.post('/', post(options));
80+
app.delete('/', del);
81+
82+
console.error(`MCP Server running on streamable HTTP on port ${port}`);
83+
84+
app.listen(port);
85+
};

packages/mcp-server/src/index.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
#!/usr/bin/env node
22

3-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4-
import { init, selectTools, server } from './server';
3+
import { selectTools } from './server';
54
import { Endpoint, endpoints } from './tools';
65
import { McpOptions, parseOptions } from './options';
6+
import { launchStdioServer } from './stdio';
7+
import { launchStreamableHTTPServer } from './http';
78

89
async function main() {
910
const options = parseOptionsOrError();
@@ -13,18 +14,21 @@ async function main() {
1314
return;
1415
}
1516

16-
const includedTools = selectToolsOrError(endpoints, options);
17+
const selectedTools = selectToolsOrError(endpoints, options);
1718

1819
console.error(
19-
`MCP Server starting with ${includedTools.length} tools:`,
20-
includedTools.map((e) => e.tool.name),
20+
`MCP Server starting with ${selectedTools.length} tools:`,
21+
selectedTools.map((e) => e.tool.name),
2122
);
2223

23-
init({ server, endpoints: includedTools });
24-
25-
const transport = new StdioServerTransport();
26-
await server.connect(transport);
27-
console.error('MCP Server running on stdio');
24+
switch (options.transport) {
25+
case 'stdio':
26+
await launchStdioServer(options, selectedTools);
27+
break;
28+
case 'http':
29+
await launchStreamableHTTPServer(options, selectedTools, options.port);
30+
break;
31+
}
2832
}
2933

3034
if (require.main === module) {
@@ -43,7 +47,7 @@ function parseOptionsOrError() {
4347
}
4448
}
4549

46-
function selectToolsOrError(endpoints: Endpoint[], options: McpOptions) {
50+
function selectToolsOrError(endpoints: Endpoint[], options: McpOptions): Endpoint[] {
4751
try {
4852
const includedTools = selectTools(endpoints, options);
4953
if (includedTools.length === 0) {

packages/mcp-server/src/options.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { ClientCapabilities, knownClients, ClientType } from './compat';
55

66
export type CLIOptions = McpOptions & {
77
list: boolean;
8+
transport: 'stdio' | 'http';
9+
port: number | undefined;
810
};
911

1012
export type McpOptions = {
@@ -129,6 +131,16 @@ export function parseOptions(): CLIOptions {
129131
type: 'boolean',
130132
description: 'Print detailed explanation of client capabilities and exit',
131133
})
134+
.option('transport', {
135+
type: 'string',
136+
choices: ['stdio', 'http'],
137+
default: 'stdio',
138+
description: 'What transport to use; stdio for local servers or http for remote servers',
139+
})
140+
.option('port', {
141+
type: 'number',
142+
description: 'Port to serve on if using http transport',
143+
})
132144
.help();
133145

134146
for (const [command, desc] of examples()) {
@@ -238,6 +250,8 @@ export function parseOptions(): CLIOptions {
238250
const includeAllTools =
239251
explicitTools ? argv.tools?.includes('all') && !argv.noTools?.includes('all') : undefined;
240252

253+
const transport = argv.transport as 'stdio' | 'http';
254+
241255
const client = argv.client as ClientType;
242256
return {
243257
client: client && knownClients[client] ? client : undefined,
@@ -246,6 +260,8 @@ export function parseOptions(): CLIOptions {
246260
filters,
247261
capabilities: clientCapabilities,
248262
list: argv.list || false,
263+
transport,
264+
port: argv.port,
249265
};
250266
}
251267

packages/mcp-server/src/server.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,17 @@ export { Filter } from './tools';
2222
export { ClientOptions } from 'isaacus';
2323
export { endpoints } from './tools';
2424

25+
export const newMcpServer = () =>
26+
new McpServer(
27+
{
28+
name: 'isaacus_api',
29+
version: '0.11.0',
30+
},
31+
{ capabilities: { tools: {}, logging: {} } },
32+
);
33+
2534
// Create server instance
26-
export const server = new McpServer(
27-
{
28-
name: 'isaacus_api',
29-
version: '0.11.0',
30-
},
31-
{ capabilities: { tools: {}, logging: {} } },
32-
);
35+
export const server = newMcpServer();
3336

3437
/**
3538
* Initializes the provided MCP Server with the given tools and handlers.
@@ -100,7 +103,7 @@ export function init(params: {
100103
/**
101104
* Selects the tools to include in the MCP Server based on the provided options.
102105
*/
103-
export function selectTools(endpoints: Endpoint[], options: McpOptions) {
106+
export function selectTools(endpoints: Endpoint[], options: McpOptions): Endpoint[] {
104107
const filteredEndpoints = query(options.filters, endpoints);
105108

106109
let includedTools = filteredEndpoints;

packages/mcp-server/src/stdio.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2+
import { init, newMcpServer } from './server';
3+
import { Endpoint } from './tools';
4+
import { McpOptions } from './options';
5+
6+
export const launchStdioServer = async (options: McpOptions, endpoints: Endpoint[]) => {
7+
const server = newMcpServer();
8+
9+
init({ server, endpoints });
10+
11+
const transport = new StdioServerTransport();
12+
await server.connect(transport);
13+
console.error('MCP Server running on stdio');
14+
};

0 commit comments

Comments
 (0)