ninerouter-mcp 1.0.1 → 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/README.md +315 -182
- package/dist/create-config.js +14 -14
- package/dist/http.js +197 -0
- package/dist/index.js +11 -4
- package/dist/ninerouter-client.js +38 -36
- package/dist/server.js +25 -18
- package/dist/tools/common.js +1 -8
- package/dist/tools/embeddings.js +17 -12
- package/dist/tools/media.js +159 -86
- package/dist/tools/models.js +12 -9
- package/dist/tools/web.js +28 -22
- package/package.json +39 -8
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 (
|
|
4
|
-
await import(
|
|
6
|
+
if (hasFlag('create-config')) {
|
|
7
|
+
await import('./create-config.js');
|
|
8
|
+
}
|
|
9
|
+
else if (hasFlag('--http')) {
|
|
10
|
+
// Start MCP server over Streamable HTTP
|
|
11
|
+
await import('./http.js');
|
|
5
12
|
}
|
|
6
13
|
else {
|
|
7
|
-
// Start MCP server
|
|
8
|
-
await import(
|
|
14
|
+
// Start MCP server over stdio
|
|
15
|
+
await import('./server.js');
|
|
9
16
|
}
|
|
10
17
|
export {};
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import { access, readFile } from
|
|
2
|
-
import os from
|
|
3
|
-
import path from
|
|
4
|
-
import { parse as parseToml } from
|
|
5
|
-
const DEFAULT_CONFIG_PATH = path.join(os.homedir(),
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parse as parseToml } from 'toml';
|
|
5
|
+
const DEFAULT_CONFIG_PATH = path.join(os.homedir(), '.config', 'ninerouter-mcp', 'config.toml');
|
|
6
6
|
export function getDefaultConfigPath() {
|
|
7
7
|
return DEFAULT_CONFIG_PATH;
|
|
8
8
|
}
|
|
9
9
|
export function resolveConfigPath(argv) {
|
|
10
10
|
for (let index = 2; index < argv.length; index += 1) {
|
|
11
11
|
const argument = argv[index];
|
|
12
|
-
if (argument ===
|
|
12
|
+
if (argument === '--config' || argument === '--config-file' || argument === '-c') {
|
|
13
13
|
const nextValue = argv[index + 1];
|
|
14
14
|
if (!nextValue) {
|
|
15
15
|
throw new Error(`Missing value for ${argument}.`);
|
|
16
16
|
}
|
|
17
17
|
return nextValue;
|
|
18
18
|
}
|
|
19
|
-
if (argument.startsWith(
|
|
20
|
-
return argument.slice(argument.indexOf(
|
|
19
|
+
if (argument.startsWith('--config=') || argument.startsWith('--config-file=')) {
|
|
20
|
+
return argument.slice(argument.indexOf('=') + 1);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
return process.env.NINEROUTER_CONFIG?.trim() || undefined;
|
|
24
24
|
}
|
|
25
25
|
function normalizeBaseUrl(baseUrl) {
|
|
26
|
-
return baseUrl.endsWith(
|
|
26
|
+
return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
|
27
27
|
}
|
|
28
28
|
function pickConfigValue(configFile, key) {
|
|
29
29
|
const nestedValue = configFile.ninerouter?.[key];
|
|
30
|
-
if (typeof nestedValue ===
|
|
30
|
+
if (typeof nestedValue === 'string' && nestedValue.trim()) {
|
|
31
31
|
return nestedValue.trim();
|
|
32
32
|
}
|
|
33
33
|
const topLevelValue = configFile[key];
|
|
34
|
-
if (typeof topLevelValue ===
|
|
34
|
+
if (typeof topLevelValue === 'string' && topLevelValue.trim()) {
|
|
35
35
|
return topLevelValue.trim();
|
|
36
36
|
}
|
|
37
37
|
return undefined;
|
|
@@ -43,14 +43,14 @@ async function loadConfigFile(configPath) {
|
|
|
43
43
|
catch {
|
|
44
44
|
return null;
|
|
45
45
|
}
|
|
46
|
-
const raw = await readFile(configPath,
|
|
46
|
+
const raw = await readFile(configPath, 'utf8');
|
|
47
47
|
const parsed = parseToml(raw);
|
|
48
48
|
return parsed;
|
|
49
49
|
}
|
|
50
50
|
export async function getConfig(options = {}) {
|
|
51
51
|
const configFile = await loadConfigFile(options.configPath ?? DEFAULT_CONFIG_PATH);
|
|
52
|
-
const baseUrlFromFile = configFile ? pickConfigValue(configFile,
|
|
53
|
-
const apiKeyFromFile = configFile ? pickConfigValue(configFile,
|
|
52
|
+
const baseUrlFromFile = configFile ? pickConfigValue(configFile, 'base_url') : undefined;
|
|
53
|
+
const apiKeyFromFile = configFile ? pickConfigValue(configFile, 'api_key') : undefined;
|
|
54
54
|
const baseUrl = baseUrlFromFile ?? process.env.NINEROUTER_URL;
|
|
55
55
|
if (!baseUrl) {
|
|
56
56
|
throw new Error(`NINEROUTER_URL is required. Set it in ${options.configPath ?? DEFAULT_CONFIG_PATH} or as an environment variable.`);
|
|
@@ -62,14 +62,16 @@ export async function getConfig(options = {}) {
|
|
|
62
62
|
return undefined;
|
|
63
63
|
return Array.isArray(value) ? value : [value];
|
|
64
64
|
};
|
|
65
|
-
const defaultModels = configFile?.default_models
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
const defaultModels = configFile?.default_models
|
|
66
|
+
? {
|
|
67
|
+
webSearch: normalizeToArray(configFile.default_models.web_search),
|
|
68
|
+
webFetch: normalizeToArray(configFile.default_models.web_fetch),
|
|
69
|
+
generateImage: normalizeToArray(configFile.default_models.generate_image),
|
|
70
|
+
textToSpeech: normalizeToArray(configFile.default_models.text_to_speech),
|
|
71
|
+
speechToText: normalizeToArray(configFile.default_models.speech_to_text),
|
|
72
|
+
embeddings: normalizeToArray(configFile.default_models.embeddings),
|
|
73
|
+
}
|
|
74
|
+
: undefined;
|
|
73
75
|
return {
|
|
74
76
|
baseUrl: normalizeBaseUrl(baseUrl),
|
|
75
77
|
apiKey: apiKey ? apiKey : undefined,
|
|
@@ -77,7 +79,7 @@ export async function getConfig(options = {}) {
|
|
|
77
79
|
};
|
|
78
80
|
}
|
|
79
81
|
function buildUrl(baseUrl, pathname, query) {
|
|
80
|
-
const url = new URL(pathname.replace(/^\//,
|
|
82
|
+
const url = new URL(pathname.replace(/^\//, ''), baseUrl);
|
|
81
83
|
for (const [key, value] of Object.entries(query ?? {})) {
|
|
82
84
|
if (value === undefined) {
|
|
83
85
|
continue;
|
|
@@ -96,15 +98,15 @@ async function readErrorMessage(response) {
|
|
|
96
98
|
}
|
|
97
99
|
try {
|
|
98
100
|
const parsed = JSON.parse(text);
|
|
99
|
-
if (typeof parsed.message ===
|
|
101
|
+
if (typeof parsed.message === 'string') {
|
|
100
102
|
return parsed.message;
|
|
101
103
|
}
|
|
102
|
-
if (typeof parsed.error ===
|
|
104
|
+
if (typeof parsed.error === 'string') {
|
|
103
105
|
return parsed.error;
|
|
104
106
|
}
|
|
105
|
-
if (parsed.error && typeof parsed.error ===
|
|
107
|
+
if (parsed.error && typeof parsed.error === 'object' && 'message' in parsed.error) {
|
|
106
108
|
const message = parsed.error.message;
|
|
107
|
-
if (typeof message ===
|
|
109
|
+
if (typeof message === 'string') {
|
|
108
110
|
return message;
|
|
109
111
|
}
|
|
110
112
|
}
|
|
@@ -124,9 +126,9 @@ function ensureOk(response, action) {
|
|
|
124
126
|
}
|
|
125
127
|
export async function requestJson(config, pathname, body, query) {
|
|
126
128
|
const response = await fetch(buildUrl(config.baseUrl, pathname, query), {
|
|
127
|
-
method:
|
|
129
|
+
method: 'POST',
|
|
128
130
|
headers: {
|
|
129
|
-
|
|
131
|
+
'Content-Type': 'application/json',
|
|
130
132
|
...authHeaders(config.apiKey),
|
|
131
133
|
},
|
|
132
134
|
body: JSON.stringify(body),
|
|
@@ -136,7 +138,7 @@ export async function requestJson(config, pathname, body, query) {
|
|
|
136
138
|
}
|
|
137
139
|
export async function requestGetJson(config, pathname, query) {
|
|
138
140
|
const response = await fetch(buildUrl(config.baseUrl, pathname, query), {
|
|
139
|
-
method:
|
|
141
|
+
method: 'GET',
|
|
140
142
|
headers: {
|
|
141
143
|
...authHeaders(config.apiKey),
|
|
142
144
|
},
|
|
@@ -146,9 +148,9 @@ export async function requestGetJson(config, pathname, query) {
|
|
|
146
148
|
}
|
|
147
149
|
export async function requestBinary(config, pathname, body, query) {
|
|
148
150
|
const response = await fetch(buildUrl(config.baseUrl, pathname, query), {
|
|
149
|
-
method:
|
|
151
|
+
method: 'POST',
|
|
150
152
|
headers: {
|
|
151
|
-
|
|
153
|
+
'Content-Type': 'application/json',
|
|
152
154
|
...authHeaders(config.apiKey),
|
|
153
155
|
},
|
|
154
156
|
body: JSON.stringify(body),
|
|
@@ -156,13 +158,13 @@ export async function requestBinary(config, pathname, body, query) {
|
|
|
156
158
|
await ensureOk(response, pathname);
|
|
157
159
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
158
160
|
return {
|
|
159
|
-
base64: buffer.toString(
|
|
160
|
-
contentType: response.headers.get(
|
|
161
|
+
base64: buffer.toString('base64'),
|
|
162
|
+
contentType: response.headers.get('content-type'),
|
|
161
163
|
};
|
|
162
164
|
}
|
|
163
165
|
export async function requestMultipartText(config, pathname, formData, query) {
|
|
164
166
|
const response = await fetch(buildUrl(config.baseUrl, pathname, query), {
|
|
165
|
-
method:
|
|
167
|
+
method: 'POST',
|
|
166
168
|
headers: {
|
|
167
169
|
...authHeaders(config.apiKey),
|
|
168
170
|
},
|
|
@@ -171,7 +173,7 @@ export async function requestMultipartText(config, pathname, formData, query) {
|
|
|
171
173
|
await ensureOk(response, pathname);
|
|
172
174
|
return {
|
|
173
175
|
text: await response.text(),
|
|
174
|
-
contentType: response.headers.get(
|
|
176
|
+
contentType: response.headers.get('content-type'),
|
|
175
177
|
};
|
|
176
178
|
}
|
|
177
179
|
export async function createFileBlob(filePath) {
|
package/dist/server.js
CHANGED
|
@@ -1,25 +1,32 @@
|
|
|
1
|
-
import { McpServer } from
|
|
2
|
-
import { StdioServerTransport } from
|
|
3
|
-
import { getConfig, resolveConfigPath } from
|
|
4
|
-
import { registerEmbeddingTools } from
|
|
5
|
-
import { registerMediaTools } from
|
|
6
|
-
import { registerModelTools } from
|
|
7
|
-
import { registerWebTools } from
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
|
|
3
|
+
import { getConfig, resolveConfigPath } from './ninerouter-client.js';
|
|
4
|
+
import { registerEmbeddingTools } from './tools/embeddings.js';
|
|
5
|
+
import { registerMediaTools } from './tools/media.js';
|
|
6
|
+
import { registerModelTools } from './tools/models.js';
|
|
7
|
+
import { registerWebTools } from './tools/web.js';
|
|
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/dist/tools/common.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
export function pickModel(model, provider) {
|
|
2
|
-
const selected = model ?? provider;
|
|
3
|
-
if (!selected) {
|
|
4
|
-
throw new Error("Provide either model or provider.");
|
|
5
|
-
}
|
|
6
|
-
return selected;
|
|
7
|
-
}
|
|
8
1
|
export function toPrettyJson(value) {
|
|
9
2
|
return JSON.stringify(value, null, 2);
|
|
10
3
|
}
|
|
@@ -21,6 +14,6 @@ export async function tryModelsWithFallback(models, operation) {
|
|
|
21
14
|
});
|
|
22
15
|
}
|
|
23
16
|
}
|
|
24
|
-
const errorSummary = errors.map((e) => `${e.model}: ${e.error}`).join(
|
|
17
|
+
const errorSummary = errors.map((e) => `${e.model}: ${e.error}`).join('; ');
|
|
25
18
|
throw new Error(`All models failed. Errors: ${errorSummary}`);
|
|
26
19
|
}
|
package/dist/tools/embeddings.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import { z } from
|
|
2
|
-
import { requestJson } from
|
|
3
|
-
import {
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { requestJson } from '../ninerouter-client.js';
|
|
3
|
+
import { tryModelsWithFallback, toPrettyJson } from './common.js';
|
|
4
4
|
export function registerEmbeddingTools(server, config) {
|
|
5
|
-
server.registerTool(
|
|
6
|
-
description:
|
|
5
|
+
server.registerTool('embeddings', {
|
|
6
|
+
description: 'Generate vector embeddings for a single string or a batch of strings. Returns the raw upstream payload (vectors, usage). Use for retrieval, semantic search, clustering, or similarity. Pass `model` (alias `provider`) to pick an embedding model (e.g. `openai/text-embedding-3-small`); otherwise the configured `default_models.embeddings` fallback chain is tried in order.',
|
|
7
7
|
inputSchema: z.object({
|
|
8
|
-
model: z
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
model: z
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('Embedding model id, for example openai/text-embedding-3-small.'),
|
|
12
|
+
provider: z.string().optional().describe('Alias for model.'),
|
|
13
|
+
input: z
|
|
14
|
+
.union([z.string(), z.array(z.string().min(1))])
|
|
15
|
+
.describe('Text input or a batch of text inputs.'),
|
|
16
|
+
encodingFormat: z.enum(['float', 'base64']).optional().default('float'),
|
|
12
17
|
dimensions: z.number().int().positive().optional(),
|
|
13
18
|
}),
|
|
14
19
|
}, async ({ model, provider, input, encodingFormat, dimensions }) => {
|
|
@@ -16,10 +21,10 @@ export function registerEmbeddingTools(server, config) {
|
|
|
16
21
|
const defaultModels = config.defaultModels?.embeddings ?? [];
|
|
17
22
|
const modelsToTry = userModel ? [userModel] : defaultModels;
|
|
18
23
|
if (modelsToTry.length === 0) {
|
|
19
|
-
throw new Error(
|
|
24
|
+
throw new Error('No model specified and no default_models.embeddings configured.');
|
|
20
25
|
}
|
|
21
26
|
const payload = await tryModelsWithFallback(modelsToTry, async (selectedModel) => {
|
|
22
|
-
return await requestJson(config,
|
|
27
|
+
return await requestJson(config, '/v1/embeddings', {
|
|
23
28
|
model: selectedModel,
|
|
24
29
|
input,
|
|
25
30
|
encoding_format: encodingFormat,
|
|
@@ -27,7 +32,7 @@ export function registerEmbeddingTools(server, config) {
|
|
|
27
32
|
});
|
|
28
33
|
});
|
|
29
34
|
return {
|
|
30
|
-
content: [{ type:
|
|
35
|
+
content: [{ type: 'text', text: toPrettyJson(payload) }],
|
|
31
36
|
};
|
|
32
37
|
});
|
|
33
38
|
}
|