@seldonframe/mcp 1.0.0 → 1.0.2
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 +112 -112
- package/package.json +42 -42
- package/src/client.js +194 -194
- package/src/index.js +81 -49
- package/src/tools.js +2001 -2001
- package/src/welcome.js +132 -80
package/src/client.js
CHANGED
|
@@ -1,194 +1,194 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { VERSION } from "./welcome.js";
|
|
5
|
-
|
|
6
|
-
const API_BASE =
|
|
7
|
-
process.env.SELDONFRAME_API_BASE ?? "https://app.seldonframe.com/api/v1";
|
|
8
|
-
const API_KEY = process.env.SELDONFRAME_API_KEY;
|
|
9
|
-
|
|
10
|
-
const SELDON_DIR = join(homedir(), ".seldonframe");
|
|
11
|
-
const DEVICE_FILE = join(SELDON_DIR, "device.json");
|
|
12
|
-
|
|
13
|
-
function loadDevice() {
|
|
14
|
-
if (!existsSync(DEVICE_FILE)) {
|
|
15
|
-
return { tokens: {}, default_workspace: null, created_at: null };
|
|
16
|
-
}
|
|
17
|
-
try {
|
|
18
|
-
const parsed = JSON.parse(readFileSync(DEVICE_FILE, "utf8"));
|
|
19
|
-
return {
|
|
20
|
-
tokens: parsed.tokens ?? {},
|
|
21
|
-
default_workspace: parsed.default_workspace ?? null,
|
|
22
|
-
created_at: parsed.created_at ?? null,
|
|
23
|
-
};
|
|
24
|
-
} catch {
|
|
25
|
-
return { tokens: {}, default_workspace: null, created_at: null };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function saveDevice(device) {
|
|
30
|
-
mkdirSync(SELDON_DIR, { recursive: true });
|
|
31
|
-
writeFileSync(DEVICE_FILE, JSON.stringify(device, null, 2));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function rememberWorkspace({ workspace_id, bearer_token, make_default = true }) {
|
|
35
|
-
const d = loadDevice();
|
|
36
|
-
if (bearer_token) d.tokens[workspace_id] = bearer_token;
|
|
37
|
-
if (make_default || !d.default_workspace) d.default_workspace = workspace_id;
|
|
38
|
-
if (!d.created_at) d.created_at = new Date().toISOString();
|
|
39
|
-
saveDevice(d);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function setDefaultWorkspace(workspace_id) {
|
|
43
|
-
const d = loadDevice();
|
|
44
|
-
d.default_workspace = workspace_id;
|
|
45
|
-
saveDevice(d);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Remove a workspace's bearer token from the local device store. If it was the
|
|
49
|
-
// default, clear the default pointer too. Used by revoke_bearer when the call
|
|
50
|
-
// revokes the caller's own token — the device can't keep pretending it has
|
|
51
|
-
// access.
|
|
52
|
-
export function forgetWorkspace(workspace_id) {
|
|
53
|
-
const d = loadDevice();
|
|
54
|
-
delete d.tokens[workspace_id];
|
|
55
|
-
if (d.default_workspace === workspace_id) {
|
|
56
|
-
const remaining = Object.keys(d.tokens);
|
|
57
|
-
d.default_workspace = remaining[0] ?? null;
|
|
58
|
-
}
|
|
59
|
-
saveDevice(d);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function getDefaultWorkspace() {
|
|
63
|
-
return loadDevice().default_workspace;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function knownWorkspaceIds() {
|
|
67
|
-
return Object.keys(loadDevice().tokens);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function hasApiKey() {
|
|
71
|
-
return !!API_KEY;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function isFirstEverCall() {
|
|
75
|
-
const d = loadDevice();
|
|
76
|
-
return Object.keys(d.tokens).length === 0 && !API_KEY;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function resolveAuth(workspace_id) {
|
|
80
|
-
if (API_KEY) return `Bearer ${API_KEY}`;
|
|
81
|
-
const d = loadDevice();
|
|
82
|
-
const target = workspace_id ?? d.default_workspace;
|
|
83
|
-
if (target && d.tokens[target]) return `Bearer ${d.tokens[target]}`;
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function getWorkspaceBearer(workspace_id) {
|
|
88
|
-
const d = loadDevice();
|
|
89
|
-
const target = workspace_id ?? d.default_workspace;
|
|
90
|
-
if (!target) return null;
|
|
91
|
-
return d.tokens[target] ?? null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function getApiKey() {
|
|
95
|
-
return API_KEY ?? null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function hintFor(status, data) {
|
|
99
|
-
if (status === 401) {
|
|
100
|
-
return " — credentials rejected. If you meant to use a paid capability, check SELDONFRAME_API_KEY at https://app.seldonframe.com/settings/api.";
|
|
101
|
-
}
|
|
102
|
-
if (status === 402) {
|
|
103
|
-
const need = data?.error?.requires ?? "a SeldonFrame API key";
|
|
104
|
-
return ` — this capability requires ${need}. Get one at https://app.seldonframe.com/settings/api and \`export SELDONFRAME_API_KEY=sk-…\`.`;
|
|
105
|
-
}
|
|
106
|
-
if (status === 409) {
|
|
107
|
-
return " — the requested resource already exists or conflicts with existing state.";
|
|
108
|
-
}
|
|
109
|
-
if (status === 429) return " — rate limited. Try again in a moment.";
|
|
110
|
-
return "";
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export async function api(method, path, opts = {}) {
|
|
114
|
-
const {
|
|
115
|
-
body,
|
|
116
|
-
workspace_id,
|
|
117
|
-
allow_anonymous = false,
|
|
118
|
-
force_workspace_bearer = false,
|
|
119
|
-
extra_headers = {},
|
|
120
|
-
} = opts;
|
|
121
|
-
|
|
122
|
-
let auth = null;
|
|
123
|
-
if (force_workspace_bearer) {
|
|
124
|
-
const token = getWorkspaceBearer(workspace_id);
|
|
125
|
-
if (token) auth = `Bearer ${token}`;
|
|
126
|
-
} else {
|
|
127
|
-
auth = resolveAuth(workspace_id);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (!auth && !allow_anonymous) {
|
|
131
|
-
throw new Error(
|
|
132
|
-
"No workspace available. Run create_workspace({ name: '…' }) first, or set SELDONFRAME_API_KEY.",
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
const headers = {
|
|
136
|
-
"Content-Type": "application/json",
|
|
137
|
-
"User-Agent": `seldonframe-mcp/${VERSION}`,
|
|
138
|
-
...extra_headers,
|
|
139
|
-
};
|
|
140
|
-
if (auth) headers.Authorization = auth;
|
|
141
|
-
|
|
142
|
-
const res = await fetch(`${API_BASE}${path}`, {
|
|
143
|
-
method,
|
|
144
|
-
headers,
|
|
145
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
146
|
-
});
|
|
147
|
-
const text = await res.text();
|
|
148
|
-
let data;
|
|
149
|
-
try {
|
|
150
|
-
data = text ? JSON.parse(text) : {};
|
|
151
|
-
} catch {
|
|
152
|
-
data = { raw: text };
|
|
153
|
-
}
|
|
154
|
-
if (!res.ok) {
|
|
155
|
-
const msg = data?.error?.message ?? data?.message ?? text ?? res.statusText;
|
|
156
|
-
throw new Error(`Seldon API ${res.status}: ${msg}${hintFor(res.status, data)}`);
|
|
157
|
-
}
|
|
158
|
-
return data;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export async function fetchText(url, { maxBytes = 256 * 1024 } = {}) {
|
|
162
|
-
const res = await fetch(url, {
|
|
163
|
-
headers: { "User-Agent": `seldonframe-mcp/${VERSION} (+soul-compiler)` },
|
|
164
|
-
redirect: "follow",
|
|
165
|
-
});
|
|
166
|
-
if (!res.ok) {
|
|
167
|
-
throw new Error(`Fetch ${url} failed: ${res.status} ${res.statusText}`);
|
|
168
|
-
}
|
|
169
|
-
const raw = await res.text();
|
|
170
|
-
const truncated = raw.length > maxBytes;
|
|
171
|
-
const html = truncated ? raw.slice(0, maxBytes) : raw;
|
|
172
|
-
return { html, truncated, status: res.status, final_url: res.url };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function htmlToText(html) {
|
|
176
|
-
return html
|
|
177
|
-
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
178
|
-
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
179
|
-
.replace(/<noscript[\s\S]*?<\/noscript>/gi, " ")
|
|
180
|
-
.replace(/<!--[\s\S]*?-->/g, " ")
|
|
181
|
-
.replace(/<(h[1-6]|p|li|br|div|section|article|header|footer)[^>]*>/gi, "\n")
|
|
182
|
-
.replace(/<[^>]+>/g, " ")
|
|
183
|
-
.replace(/ /g, " ")
|
|
184
|
-
.replace(/&/g, "&")
|
|
185
|
-
.replace(/</g, "<")
|
|
186
|
-
.replace(/>/g, ">")
|
|
187
|
-
.replace(/"/g, '"')
|
|
188
|
-
.replace(/'/g, "'")
|
|
189
|
-
.replace(/[ \t]+/g, " ")
|
|
190
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
191
|
-
.trim();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export const API_INFO = { base: API_BASE };
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { VERSION } from "./welcome.js";
|
|
5
|
+
|
|
6
|
+
const API_BASE =
|
|
7
|
+
process.env.SELDONFRAME_API_BASE ?? "https://app.seldonframe.com/api/v1";
|
|
8
|
+
const API_KEY = process.env.SELDONFRAME_API_KEY;
|
|
9
|
+
|
|
10
|
+
const SELDON_DIR = join(homedir(), ".seldonframe");
|
|
11
|
+
const DEVICE_FILE = join(SELDON_DIR, "device.json");
|
|
12
|
+
|
|
13
|
+
function loadDevice() {
|
|
14
|
+
if (!existsSync(DEVICE_FILE)) {
|
|
15
|
+
return { tokens: {}, default_workspace: null, created_at: null };
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(readFileSync(DEVICE_FILE, "utf8"));
|
|
19
|
+
return {
|
|
20
|
+
tokens: parsed.tokens ?? {},
|
|
21
|
+
default_workspace: parsed.default_workspace ?? null,
|
|
22
|
+
created_at: parsed.created_at ?? null,
|
|
23
|
+
};
|
|
24
|
+
} catch {
|
|
25
|
+
return { tokens: {}, default_workspace: null, created_at: null };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function saveDevice(device) {
|
|
30
|
+
mkdirSync(SELDON_DIR, { recursive: true });
|
|
31
|
+
writeFileSync(DEVICE_FILE, JSON.stringify(device, null, 2));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function rememberWorkspace({ workspace_id, bearer_token, make_default = true }) {
|
|
35
|
+
const d = loadDevice();
|
|
36
|
+
if (bearer_token) d.tokens[workspace_id] = bearer_token;
|
|
37
|
+
if (make_default || !d.default_workspace) d.default_workspace = workspace_id;
|
|
38
|
+
if (!d.created_at) d.created_at = new Date().toISOString();
|
|
39
|
+
saveDevice(d);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function setDefaultWorkspace(workspace_id) {
|
|
43
|
+
const d = loadDevice();
|
|
44
|
+
d.default_workspace = workspace_id;
|
|
45
|
+
saveDevice(d);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Remove a workspace's bearer token from the local device store. If it was the
|
|
49
|
+
// default, clear the default pointer too. Used by revoke_bearer when the call
|
|
50
|
+
// revokes the caller's own token — the device can't keep pretending it has
|
|
51
|
+
// access.
|
|
52
|
+
export function forgetWorkspace(workspace_id) {
|
|
53
|
+
const d = loadDevice();
|
|
54
|
+
delete d.tokens[workspace_id];
|
|
55
|
+
if (d.default_workspace === workspace_id) {
|
|
56
|
+
const remaining = Object.keys(d.tokens);
|
|
57
|
+
d.default_workspace = remaining[0] ?? null;
|
|
58
|
+
}
|
|
59
|
+
saveDevice(d);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getDefaultWorkspace() {
|
|
63
|
+
return loadDevice().default_workspace;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function knownWorkspaceIds() {
|
|
67
|
+
return Object.keys(loadDevice().tokens);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function hasApiKey() {
|
|
71
|
+
return !!API_KEY;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isFirstEverCall() {
|
|
75
|
+
const d = loadDevice();
|
|
76
|
+
return Object.keys(d.tokens).length === 0 && !API_KEY;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveAuth(workspace_id) {
|
|
80
|
+
if (API_KEY) return `Bearer ${API_KEY}`;
|
|
81
|
+
const d = loadDevice();
|
|
82
|
+
const target = workspace_id ?? d.default_workspace;
|
|
83
|
+
if (target && d.tokens[target]) return `Bearer ${d.tokens[target]}`;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getWorkspaceBearer(workspace_id) {
|
|
88
|
+
const d = loadDevice();
|
|
89
|
+
const target = workspace_id ?? d.default_workspace;
|
|
90
|
+
if (!target) return null;
|
|
91
|
+
return d.tokens[target] ?? null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getApiKey() {
|
|
95
|
+
return API_KEY ?? null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function hintFor(status, data) {
|
|
99
|
+
if (status === 401) {
|
|
100
|
+
return " — credentials rejected. If you meant to use a paid capability, check SELDONFRAME_API_KEY at https://app.seldonframe.com/settings/api.";
|
|
101
|
+
}
|
|
102
|
+
if (status === 402) {
|
|
103
|
+
const need = data?.error?.requires ?? "a SeldonFrame API key";
|
|
104
|
+
return ` — this capability requires ${need}. Get one at https://app.seldonframe.com/settings/api and \`export SELDONFRAME_API_KEY=sk-…\`.`;
|
|
105
|
+
}
|
|
106
|
+
if (status === 409) {
|
|
107
|
+
return " — the requested resource already exists or conflicts with existing state.";
|
|
108
|
+
}
|
|
109
|
+
if (status === 429) return " — rate limited. Try again in a moment.";
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function api(method, path, opts = {}) {
|
|
114
|
+
const {
|
|
115
|
+
body,
|
|
116
|
+
workspace_id,
|
|
117
|
+
allow_anonymous = false,
|
|
118
|
+
force_workspace_bearer = false,
|
|
119
|
+
extra_headers = {},
|
|
120
|
+
} = opts;
|
|
121
|
+
|
|
122
|
+
let auth = null;
|
|
123
|
+
if (force_workspace_bearer) {
|
|
124
|
+
const token = getWorkspaceBearer(workspace_id);
|
|
125
|
+
if (token) auth = `Bearer ${token}`;
|
|
126
|
+
} else {
|
|
127
|
+
auth = resolveAuth(workspace_id);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!auth && !allow_anonymous) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
"No workspace available. Run create_workspace({ name: '…' }) first, or set SELDONFRAME_API_KEY.",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const headers = {
|
|
136
|
+
"Content-Type": "application/json",
|
|
137
|
+
"User-Agent": `seldonframe-mcp/${VERSION}`,
|
|
138
|
+
...extra_headers,
|
|
139
|
+
};
|
|
140
|
+
if (auth) headers.Authorization = auth;
|
|
141
|
+
|
|
142
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
143
|
+
method,
|
|
144
|
+
headers,
|
|
145
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
146
|
+
});
|
|
147
|
+
const text = await res.text();
|
|
148
|
+
let data;
|
|
149
|
+
try {
|
|
150
|
+
data = text ? JSON.parse(text) : {};
|
|
151
|
+
} catch {
|
|
152
|
+
data = { raw: text };
|
|
153
|
+
}
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
const msg = data?.error?.message ?? data?.message ?? text ?? res.statusText;
|
|
156
|
+
throw new Error(`Seldon API ${res.status}: ${msg}${hintFor(res.status, data)}`);
|
|
157
|
+
}
|
|
158
|
+
return data;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function fetchText(url, { maxBytes = 256 * 1024 } = {}) {
|
|
162
|
+
const res = await fetch(url, {
|
|
163
|
+
headers: { "User-Agent": `seldonframe-mcp/${VERSION} (+soul-compiler)` },
|
|
164
|
+
redirect: "follow",
|
|
165
|
+
});
|
|
166
|
+
if (!res.ok) {
|
|
167
|
+
throw new Error(`Fetch ${url} failed: ${res.status} ${res.statusText}`);
|
|
168
|
+
}
|
|
169
|
+
const raw = await res.text();
|
|
170
|
+
const truncated = raw.length > maxBytes;
|
|
171
|
+
const html = truncated ? raw.slice(0, maxBytes) : raw;
|
|
172
|
+
return { html, truncated, status: res.status, final_url: res.url };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function htmlToText(html) {
|
|
176
|
+
return html
|
|
177
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
178
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
179
|
+
.replace(/<noscript[\s\S]*?<\/noscript>/gi, " ")
|
|
180
|
+
.replace(/<!--[\s\S]*?-->/g, " ")
|
|
181
|
+
.replace(/<(h[1-6]|p|li|br|div|section|article|header|footer)[^>]*>/gi, "\n")
|
|
182
|
+
.replace(/<[^>]+>/g, " ")
|
|
183
|
+
.replace(/ /g, " ")
|
|
184
|
+
.replace(/&/g, "&")
|
|
185
|
+
.replace(/</g, "<")
|
|
186
|
+
.replace(/>/g, ">")
|
|
187
|
+
.replace(/"/g, '"')
|
|
188
|
+
.replace(/'/g, "'")
|
|
189
|
+
.replace(/[ \t]+/g, " ")
|
|
190
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
191
|
+
.trim();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const API_INFO = { base: API_BASE };
|
package/src/index.js
CHANGED
|
@@ -1,49 +1,81 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Node version gate — must run before any imports that could fail or
|
|
4
|
+
// reach for fetch(). Without this, Node 16 users get a cryptic
|
|
5
|
+
// "fetch is not defined" crash the moment the first tool calls the
|
|
6
|
+
// SeldonFrame API.
|
|
7
|
+
//
|
|
8
|
+
// Per L-29 the cleanroom test in GitHub Codespaces (default image
|
|
9
|
+
// ships Node v16.20.2 in some configurations) surfaced this
|
|
10
|
+
// immediately on @seldonframe/mcp@1.0.0. Strict requirement is
|
|
11
|
+
// documented in engines.node = ">=18" and reinforced here at runtime
|
|
12
|
+
// so users get actionable next-steps instead of a stack trace.
|
|
13
|
+
//
|
|
14
|
+
// Implementation note: we use dynamic `await import()` for the SDK
|
|
15
|
+
// modules below specifically so this gate runs BEFORE any SDK code
|
|
16
|
+
// executes. Static `import` statements at the top of an ESM module
|
|
17
|
+
// are hoisted and evaluated before any module-body code, which would
|
|
18
|
+
// defeat the gate if the SDK ever started failing at import-time on
|
|
19
|
+
// older Node versions.
|
|
20
|
+
const [nodeMajor] = process.versions.node.split(".").map(Number);
|
|
21
|
+
if (nodeMajor < 18) {
|
|
22
|
+
process.stderr.write(
|
|
23
|
+
`\n SeldonFrame MCP requires Node.js 18 or later.\n` +
|
|
24
|
+
` You are running Node.js ${process.versions.node}.\n\n` +
|
|
25
|
+
` To fix:\n` +
|
|
26
|
+
` nvm install 18 && nvm use 18\n` +
|
|
27
|
+
` Or:\n` +
|
|
28
|
+
` nvm install 20 && nvm use 20\n\n` +
|
|
29
|
+
` Then retry:\n` +
|
|
30
|
+
` claude mcp add seldonframe -- npx -y @seldonframe/mcp\n\n`,
|
|
31
|
+
);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
|
|
36
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
37
|
+
const { CallToolRequestSchema, ListToolsRequestSchema } = await import(
|
|
38
|
+
"@modelcontextprotocol/sdk/types.js"
|
|
39
|
+
);
|
|
40
|
+
const { WELCOME_MARKDOWN, VERSION } = await import("./welcome.js");
|
|
41
|
+
const { TOOLS, TOOL_MAP } = await import("./tools.js");
|
|
42
|
+
|
|
43
|
+
const server = new Server(
|
|
44
|
+
{ name: "seldonframe", version: VERSION },
|
|
45
|
+
{
|
|
46
|
+
capabilities: { tools: {} },
|
|
47
|
+
instructions: WELCOME_MARKDOWN,
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
52
|
+
tools: TOOLS.map(({ name, description, inputSchema }) => ({
|
|
53
|
+
name,
|
|
54
|
+
description,
|
|
55
|
+
inputSchema,
|
|
56
|
+
})),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
60
|
+
const tool = TOOL_MAP[req.params.name];
|
|
61
|
+
if (!tool) {
|
|
62
|
+
return {
|
|
63
|
+
isError: true,
|
|
64
|
+
content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const result = await tool.handler(req.params.arguments ?? {});
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
71
|
+
};
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return {
|
|
74
|
+
isError: true,
|
|
75
|
+
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const transport = new StdioServerTransport();
|
|
81
|
+
await server.connect(transport);
|