ninerouter-mcp 1.1.0 → 1.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http.js ADDED
@@ -0,0 +1,197 @@
1
+ import { createServer } from 'node:http';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { WebStandardStreamableHTTPServerTransport, isJsonContentType, } from '@modelcontextprotocol/server';
4
+ import { getConfig, resolveConfigPath } from './ninerouter-client.js';
5
+ import { buildServer, registerTools } from './server.js';
6
+ function parseHttpArgs(argv) {
7
+ let port = 3000;
8
+ let host = '127.0.0.1';
9
+ for (let index = 2; index < argv.length; index += 1) {
10
+ const argument = argv[index];
11
+ const nextValue = argv[index + 1];
12
+ if (argument === '--port' || argument === '-p') {
13
+ if (!nextValue)
14
+ throw new Error(`Missing value for ${argument}.`);
15
+ const parsed = Number.parseInt(nextValue, 10);
16
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
17
+ throw new Error(`Invalid port: ${nextValue}`);
18
+ }
19
+ port = parsed;
20
+ index += 1;
21
+ }
22
+ else if (argument === '--host') {
23
+ if (!nextValue)
24
+ throw new Error(`Missing value for --host.`);
25
+ host = nextValue;
26
+ index += 1;
27
+ }
28
+ }
29
+ return { port, host, path: '/mcp', configPath: resolveConfigPath(argv) };
30
+ }
31
+ function toWebRequest(req) {
32
+ const host = req.headers.host ?? 'localhost';
33
+ const protocol = 'http';
34
+ const url = `${protocol}://${host}${req.url ?? '/'}`;
35
+ const headers = new Headers();
36
+ for (const [name, value] of Object.entries(req.headers)) {
37
+ if (value === undefined)
38
+ continue;
39
+ if (Array.isArray(value)) {
40
+ for (const v of value)
41
+ headers.append(name, v);
42
+ }
43
+ else {
44
+ headers.set(name, value);
45
+ }
46
+ }
47
+ const init = { method: req.method, headers };
48
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
49
+ init.body = new ReadableStream({
50
+ start(controller) {
51
+ req.on('data', (chunk) => controller.enqueue(new Uint8Array(chunk)));
52
+ req.on('end', () => controller.close());
53
+ req.on('error', (error) => controller.error(error));
54
+ },
55
+ });
56
+ init.duplex = 'half';
57
+ }
58
+ return new Request(url, init);
59
+ }
60
+ async function sendWebResponse(nodeResponse, response) {
61
+ applyCors(nodeResponse);
62
+ nodeResponse.statusCode = response.status;
63
+ response.headers.forEach((value, name) => {
64
+ if (name.toLowerCase() === 'set-cookie')
65
+ return;
66
+ if (name.toLowerCase().startsWith('access-control-'))
67
+ return;
68
+ nodeResponse.setHeader(name, value);
69
+ });
70
+ const setCookies = response.headers.getSetCookie?.() ?? [];
71
+ for (const cookie of setCookies)
72
+ nodeResponse.appendHeader('set-cookie', cookie);
73
+ if (response.body) {
74
+ const reader = response.body.getReader();
75
+ try {
76
+ while (true) {
77
+ const { done, value } = await reader.read();
78
+ if (done)
79
+ break;
80
+ if (value)
81
+ nodeResponse.write(Buffer.from(value));
82
+ }
83
+ }
84
+ finally {
85
+ reader.releaseLock();
86
+ }
87
+ }
88
+ nodeResponse.end();
89
+ }
90
+ const CORS_HEADERS = {
91
+ 'access-control-allow-origin': '*',
92
+ 'access-control-allow-methods': 'GET, POST, DELETE, OPTIONS',
93
+ 'access-control-allow-headers': 'content-type, mcp-session-id, mcp-protocol-version, mcp-method, mcp-name, accept',
94
+ 'access-control-expose-headers': 'mcp-session-id, mcp-protocol-version',
95
+ 'access-control-max-age': '86400',
96
+ };
97
+ function applyCors(res) {
98
+ for (const [name, value] of Object.entries(CORS_HEADERS)) {
99
+ res.setHeader(name, value);
100
+ }
101
+ }
102
+ async function main() {
103
+ const args = parseHttpArgs(process.argv);
104
+ const config = await getConfig({ configPath: args.configPath });
105
+ const sessions = new Map();
106
+ const createSession = async () => {
107
+ const server = buildServer();
108
+ registerTools(server, config);
109
+ const transport = new WebStandardStreamableHTTPServerTransport({
110
+ sessionIdGenerator: () => randomUUID(),
111
+ onsessioninitialized: (sessionId) => {
112
+ sessions.set(sessionId, { transport, server });
113
+ },
114
+ onsessionclosed: (sessionId) => {
115
+ sessions.delete(sessionId);
116
+ },
117
+ });
118
+ await server.connect(transport);
119
+ return { transport, server };
120
+ };
121
+ const server = createServer(async (req, res) => {
122
+ const start = Date.now();
123
+ const log = (status) => {
124
+ const ms = Date.now() - start;
125
+ const size = Number(res.getHeader('content-length')) || 0;
126
+ const mcpMethod = req.headers['mcp-method'];
127
+ const tag = mcpMethod ? ` ${String(mcpMethod)}` : '';
128
+ const sid = req.headers['mcp-session-id'];
129
+ const sidTag = sid ? ` sid=${String(sid).slice(0, 8)}` : '';
130
+ process.stderr.write(`[${new Date().toISOString()}] ${req.method} ${req.url}${tag}${sidTag} → ${status} ${ms}ms ${size}B\n`);
131
+ };
132
+ try {
133
+ const url = new URL(req.url ?? '/', 'http://localhost');
134
+ if (url.pathname !== args.path) {
135
+ applyCors(res);
136
+ res.statusCode = 404;
137
+ res.setHeader('content-type', 'text/plain');
138
+ res.end('Not Found');
139
+ log(404);
140
+ return;
141
+ }
142
+ if (req.method === 'OPTIONS') {
143
+ applyCors(res);
144
+ res.statusCode = 204;
145
+ res.end();
146
+ log(204);
147
+ return;
148
+ }
149
+ if (req.method === 'POST' && !isJsonContentType(req.headers['content-type'] ?? null)) {
150
+ applyCors(res);
151
+ res.statusCode = 415;
152
+ res.setHeader('content-type', 'text/plain');
153
+ res.end('Content-Type must be application/json');
154
+ log(415);
155
+ return;
156
+ }
157
+ const sessionHeader = req.headers['mcp-session-id'];
158
+ const sessionId = typeof sessionHeader === 'string' ? sessionHeader : undefined;
159
+ let transport;
160
+ if (sessionId && sessions.has(sessionId)) {
161
+ transport = sessions.get(sessionId).transport;
162
+ }
163
+ else {
164
+ const created = await createSession();
165
+ transport = created.transport;
166
+ }
167
+ const request = toWebRequest(req);
168
+ const response = await transport.handleRequest(request);
169
+ await sendWebResponse(res, response);
170
+ log(response.status);
171
+ }
172
+ catch (error) {
173
+ console.error(error);
174
+ if (!res.headersSent) {
175
+ applyCors(res);
176
+ res.statusCode = 500;
177
+ res.setHeader('content-type', 'text/plain');
178
+ }
179
+ res.end('Internal Server Error');
180
+ log(500);
181
+ }
182
+ });
183
+ await new Promise((resolve) => server.listen(args.port, args.host, resolve));
184
+ console.error(`ninerouter-mcp HTTP listening on http://${args.host}:${args.port}${args.path}`);
185
+ const shutdown = () => {
186
+ server.close(() => {
187
+ for (const { transport, server: s } of sessions.values()) {
188
+ void transport.close();
189
+ void s.close?.();
190
+ }
191
+ process.exit(0);
192
+ });
193
+ };
194
+ process.on('SIGINT', shutdown);
195
+ process.on('SIGTERM', shutdown);
196
+ }
197
+ void main();
package/dist/index.js CHANGED
@@ -1,10 +1,17 @@
1
1
  #!/usr/bin/env node
2
+ function hasFlag(flag) {
3
+ return process.argv.includes(flag);
4
+ }
2
5
  // Check if user wants to create config
3
- if (process.argv.includes('create-config')) {
6
+ if (hasFlag('create-config')) {
4
7
  await import('./create-config.js');
5
8
  }
9
+ else if (hasFlag('--http')) {
10
+ // Start MCP server over Streamable HTTP
11
+ await import('./http.js');
12
+ }
6
13
  else {
7
- // Start MCP server
14
+ // Start MCP server over stdio
8
15
  await import('./server.js');
9
16
  }
10
17
  export {};
package/dist/server.js CHANGED
@@ -5,21 +5,28 @@ import { registerEmbeddingTools } from './tools/embeddings.js';
5
5
  import { registerMediaTools } from './tools/media.js';
6
6
  import { registerModelTools } from './tools/models.js';
7
7
  import { registerWebTools } from './tools/web.js';
8
- const server = new McpServer({
9
- name: 'ninerouter-mcp',
10
- version: '1.0.0',
11
- }, {
12
- capabilities: {
13
- tools: {},
14
- },
15
- });
16
- async function main() {
17
- const configPath = resolveConfigPath(process.argv);
18
- const config = await getConfig({ configPath });
8
+ export function buildServer() {
9
+ const server = new McpServer({
10
+ name: 'ninerouter-mcp',
11
+ version: '1.0.0',
12
+ }, {
13
+ capabilities: {
14
+ tools: {},
15
+ },
16
+ });
17
+ return server;
18
+ }
19
+ export function registerTools(server, config) {
19
20
  registerModelTools(server, config);
20
21
  registerWebTools(server, config);
21
22
  registerMediaTools(server, config);
22
23
  registerEmbeddingTools(server, config);
24
+ }
25
+ async function main() {
26
+ const configPath = resolveConfigPath(process.argv);
27
+ const config = await getConfig({ configPath });
28
+ const server = buildServer();
29
+ registerTools(server, config);
23
30
  const transport = new StdioServerTransport();
24
31
  await server.connect(transport);
25
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ninerouter-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0-beta.1",
4
4
  "type": "module",
5
5
  "description": "MCP server for 9Router with model discovery, automatic fallback, and type-safe validation",
6
6
  "license": "Apache-2.0",
@@ -59,9 +59,11 @@
59
59
  },
60
60
  "devDependencies": {
61
61
  "@eslint/js": "^10.0.1",
62
+ "@semantic-release/exec": "^7.1.0",
62
63
  "@semantic-release/git": "^10.0.1",
63
64
  "@semantic-release/npm": "^13.1.5",
64
65
  "@types/node": "^22.20.0",
66
+ "conventional-changelog-conventionalcommits": "^9.1.0",
65
67
  "eslint": "^10.7.0",
66
68
  "husky": "^9.1.7",
67
69
  "lint-staged": "^16.0.0",