roboport 0.1.0 → 0.2.0
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/mcp/auth.d.ts +2 -0
- package/mcp/index.d.ts +4 -1
- package/mcp/index.js +811 -801
- package/package.json +1 -1
package/mcp/index.js
CHANGED
|
@@ -1,887 +1,889 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/
|
|
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
|
-
while (this.waiters.length > 0) {
|
|
37
|
-
const waiter = this.waiters.shift();
|
|
38
|
-
waiter?.({ value: undefined, done: true });
|
|
2
|
+
// src/mcp/oauth.ts
|
|
3
|
+
function base64UrlEncode(bytes) {
|
|
4
|
+
let str = "";
|
|
5
|
+
for (const byte of bytes)
|
|
6
|
+
str += String.fromCharCode(byte);
|
|
7
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
8
|
+
}
|
|
9
|
+
function randomBytes(length) {
|
|
10
|
+
const bytes = new Uint8Array(length);
|
|
11
|
+
crypto.getRandomValues(bytes);
|
|
12
|
+
return bytes;
|
|
13
|
+
}
|
|
14
|
+
async function sha256(input) {
|
|
15
|
+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
16
|
+
return new Uint8Array(hash);
|
|
17
|
+
}
|
|
18
|
+
async function generatePkce() {
|
|
19
|
+
const verifier = base64UrlEncode(randomBytes(32));
|
|
20
|
+
const challenge = base64UrlEncode(await sha256(verifier));
|
|
21
|
+
return { verifier, challenge };
|
|
22
|
+
}
|
|
23
|
+
function generateState() {
|
|
24
|
+
return base64UrlEncode(randomBytes(16));
|
|
25
|
+
}
|
|
26
|
+
async function discover(serverUrl) {
|
|
27
|
+
const u = new URL(serverUrl);
|
|
28
|
+
const origin = `${u.protocol}//${u.host}`;
|
|
29
|
+
let authServer = origin;
|
|
30
|
+
try {
|
|
31
|
+
const res2 = await fetch(`${origin}/.well-known/oauth-protected-resource`);
|
|
32
|
+
if (res2.ok) {
|
|
33
|
+
const meta = await res2.json();
|
|
34
|
+
if (meta.authorization_servers?.[0])
|
|
35
|
+
authServer = meta.authorization_servers[0];
|
|
39
36
|
}
|
|
37
|
+
} catch {}
|
|
38
|
+
const asUrl = new URL(authServer);
|
|
39
|
+
const asOrigin = `${asUrl.protocol}//${asUrl.host}`;
|
|
40
|
+
const res = await fetch(`${asOrigin}/.well-known/oauth-authorization-server`);
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
throw new Error(`OAuth discovery failed for ${asOrigin}: ${res.status} ${await res.text()}`);
|
|
40
43
|
}
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
return await res.json();
|
|
45
|
+
}
|
|
46
|
+
async function registerClient(registrationEndpoint, redirectUri) {
|
|
47
|
+
const res = await fetch(registrationEndpoint, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { "content-type": "application/json" },
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
client_name: "roboport",
|
|
52
|
+
redirect_uris: [redirectUri],
|
|
53
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
54
|
+
response_types: ["code"],
|
|
55
|
+
token_endpoint_auth_method: "none"
|
|
56
|
+
})
|
|
57
|
+
});
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
throw new Error(`OAuth client registration failed: ${res.status} ${await res.text()}`);
|
|
43
60
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const data = await res.json();
|
|
62
|
+
return data.client_id;
|
|
63
|
+
}
|
|
64
|
+
function openBrowser(url) {
|
|
65
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
66
|
+
Bun.spawn([cmd, url], { stdout: "ignore", stderr: "ignore" });
|
|
67
|
+
}
|
|
68
|
+
function captureAuthorizationCode(port, expectedState, timeoutMs) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const server = Bun.serve({
|
|
71
|
+
port,
|
|
72
|
+
hostname: "127.0.0.1",
|
|
73
|
+
fetch(req) {
|
|
74
|
+
const u = new URL(req.url);
|
|
75
|
+
const error = u.searchParams.get("error");
|
|
76
|
+
if (error) {
|
|
77
|
+
const desc = u.searchParams.get("error_description") ?? "";
|
|
78
|
+
finish(() => reject(new Error(`OAuth error: ${error} ${desc}`)));
|
|
79
|
+
return new Response("Authentication failed. You can close this tab.", {
|
|
80
|
+
status: 400
|
|
59
81
|
});
|
|
60
82
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
};
|
|
83
|
+
const code = u.searchParams.get("code");
|
|
84
|
+
const state = u.searchParams.get("state");
|
|
85
|
+
if (!code || state !== expectedState) {
|
|
86
|
+
finish(() => reject(new Error("OAuth state mismatch or missing code.")));
|
|
87
|
+
return new Response("Invalid OAuth response.", { status: 400 });
|
|
88
|
+
}
|
|
89
|
+
finish(() => resolve(code));
|
|
90
|
+
return new Response("<html><body>Authenticated. You can close this tab.</body></html>", { headers: { "content-type": "text/html" } });
|
|
70
91
|
}
|
|
71
|
-
};
|
|
92
|
+
});
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
finish(() => reject(new Error("OAuth flow timed out.")));
|
|
95
|
+
}, timeoutMs);
|
|
96
|
+
function finish(action) {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
setTimeout(() => server.stop(true), 50);
|
|
99
|
+
action();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async function exchangeCode(opts) {
|
|
104
|
+
const body = new URLSearchParams({
|
|
105
|
+
grant_type: "authorization_code",
|
|
106
|
+
code: opts.code,
|
|
107
|
+
redirect_uri: opts.redirectUri,
|
|
108
|
+
client_id: opts.clientId,
|
|
109
|
+
code_verifier: opts.verifier
|
|
110
|
+
});
|
|
111
|
+
const res = await fetch(opts.tokenEndpoint, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
114
|
+
body
|
|
115
|
+
});
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
throw new Error(`OAuth token exchange failed: ${res.status} ${await res.text()}`);
|
|
72
118
|
}
|
|
73
|
-
|
|
74
|
-
|
|
119
|
+
return await res.json();
|
|
120
|
+
}
|
|
121
|
+
async function refreshTokens(opts) {
|
|
122
|
+
const body = new URLSearchParams({
|
|
123
|
+
grant_type: "refresh_token",
|
|
124
|
+
refresh_token: opts.refreshToken,
|
|
125
|
+
client_id: opts.clientId
|
|
126
|
+
});
|
|
127
|
+
const res = await fetch(opts.tokenEndpoint, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
130
|
+
body
|
|
131
|
+
});
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
throw new Error(`OAuth refresh failed: ${res.status} ${await res.text()}`);
|
|
75
134
|
}
|
|
135
|
+
return await res.json();
|
|
76
136
|
}
|
|
77
137
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
138
|
+
// src/mcp/storage.ts
|
|
139
|
+
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
140
|
+
import { homedir } from "os";
|
|
141
|
+
import { dirname, join } from "path";
|
|
142
|
+
var DEFAULT_PATH = join(homedir(), ".roboport", "mcp-auth.json");
|
|
143
|
+
|
|
144
|
+
class FileStorage {
|
|
145
|
+
path;
|
|
146
|
+
cache;
|
|
147
|
+
constructor(path) {
|
|
148
|
+
this.path = path ?? DEFAULT_PATH;
|
|
84
149
|
}
|
|
85
|
-
|
|
86
|
-
|
|
150
|
+
async read() {
|
|
151
|
+
if (this.cache)
|
|
152
|
+
return this.cache;
|
|
153
|
+
try {
|
|
154
|
+
const raw = await readFile(this.path, "utf8");
|
|
155
|
+
this.cache = JSON.parse(raw);
|
|
156
|
+
} catch {
|
|
157
|
+
this.cache = {};
|
|
158
|
+
}
|
|
159
|
+
return this.cache;
|
|
87
160
|
}
|
|
88
|
-
|
|
89
|
-
|
|
161
|
+
async write(data) {
|
|
162
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
163
|
+
await writeFile(this.path, JSON.stringify(data, null, 2), "utf8");
|
|
164
|
+
await chmod(this.path, 384);
|
|
165
|
+
this.cache = data;
|
|
90
166
|
}
|
|
91
|
-
async
|
|
92
|
-
await this.
|
|
167
|
+
async load(key) {
|
|
168
|
+
const data = await this.read();
|
|
169
|
+
return data[key] ?? null;
|
|
93
170
|
}
|
|
94
|
-
async
|
|
95
|
-
await this.
|
|
171
|
+
async save(key, tokens) {
|
|
172
|
+
const data = await this.read();
|
|
173
|
+
data[key] = tokens;
|
|
174
|
+
await this.write(data);
|
|
175
|
+
}
|
|
176
|
+
async clear(key) {
|
|
177
|
+
const data = await this.read();
|
|
178
|
+
delete data[key];
|
|
179
|
+
await this.write(data);
|
|
96
180
|
}
|
|
97
181
|
}
|
|
98
182
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
class Tool {
|
|
106
|
-
name;
|
|
107
|
-
description;
|
|
108
|
-
inputSchema;
|
|
109
|
-
jsonSchema;
|
|
110
|
-
execute;
|
|
111
|
-
deferred;
|
|
112
|
-
constructor(init) {
|
|
113
|
-
this.name = init.name;
|
|
114
|
-
this.description = init.description;
|
|
115
|
-
this.deferred = init.deferred ?? false;
|
|
116
|
-
if ("inputSchema" in init) {
|
|
117
|
-
this.inputSchema = init.inputSchema;
|
|
118
|
-
this.execute = init.execute;
|
|
119
|
-
} else {
|
|
120
|
-
this.jsonSchema = init.jsonSchema;
|
|
121
|
-
this.execute = init.execute;
|
|
122
|
-
}
|
|
183
|
+
class MemoryStorage {
|
|
184
|
+
data = new Map;
|
|
185
|
+
async load(key) {
|
|
186
|
+
return this.data.get(key) ?? null;
|
|
123
187
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return this.jsonSchema;
|
|
127
|
-
if (this.inputSchema === undefined) {
|
|
128
|
-
throw new Error(`Tool "${this.name}" has neither inputSchema nor jsonSchema.`);
|
|
129
|
-
}
|
|
130
|
-
return z4.toJSONSchema(this.inputSchema);
|
|
188
|
+
async save(key, tokens) {
|
|
189
|
+
this.data.set(key, tokens);
|
|
131
190
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (!schema)
|
|
135
|
-
return input;
|
|
136
|
-
if (hasParseMethod(schema))
|
|
137
|
-
return schema.parse(input);
|
|
138
|
-
return z4.parse(schema, input);
|
|
191
|
+
async clear(key) {
|
|
192
|
+
this.data.delete(key);
|
|
139
193
|
}
|
|
140
194
|
}
|
|
141
|
-
function createRegistry(tools) {
|
|
142
|
-
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
143
|
-
const loadedNames = new Set(tools.filter((tool) => !tool.deferred).map((tool) => tool.name));
|
|
144
|
-
return {
|
|
145
|
-
loaded: () => tools.filter((tool) => loadedNames.has(tool.name)),
|
|
146
|
-
deferred: () => tools.filter((tool) => tool.deferred && !loadedNames.has(tool.name)),
|
|
147
|
-
load: (names) => {
|
|
148
|
-
const loaded = [];
|
|
149
|
-
const missing = [];
|
|
150
|
-
for (const name of names) {
|
|
151
|
-
const tool = byName.get(name);
|
|
152
|
-
if (!tool) {
|
|
153
|
-
missing.push(name);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
loadedNames.add(name);
|
|
157
|
-
loaded.push(tool);
|
|
158
|
-
}
|
|
159
|
-
return { loaded, missing };
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
195
|
|
|
164
|
-
// src/
|
|
165
|
-
class
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
skills;
|
|
170
|
-
mcp;
|
|
171
|
-
cwd;
|
|
172
|
-
registrations = [];
|
|
173
|
-
unsubs = [];
|
|
174
|
-
constructor({
|
|
175
|
-
model,
|
|
176
|
-
system,
|
|
177
|
-
tools,
|
|
178
|
-
skills,
|
|
179
|
-
mcp,
|
|
180
|
-
cwd
|
|
181
|
-
}) {
|
|
182
|
-
this.model = model;
|
|
183
|
-
this.system = system;
|
|
184
|
-
this.tools = tools;
|
|
185
|
-
this.skills = skills;
|
|
186
|
-
this.mcp = mcp ?? [];
|
|
187
|
-
this.cwd = cwd;
|
|
196
|
+
// src/mcp/auth.ts
|
|
197
|
+
class BearerAuth {
|
|
198
|
+
token;
|
|
199
|
+
constructor(token) {
|
|
200
|
+
this.token = token;
|
|
188
201
|
}
|
|
189
|
-
|
|
190
|
-
this.
|
|
202
|
+
async getHeader() {
|
|
203
|
+
return `Bearer ${this.token}`;
|
|
191
204
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
205
|
+
}
|
|
206
|
+
var DEFAULT_PORT = 33418;
|
|
207
|
+
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
208
|
+
|
|
209
|
+
class OAuthAuth {
|
|
210
|
+
serverUrl;
|
|
211
|
+
storage;
|
|
212
|
+
storageKey;
|
|
213
|
+
redirectPort;
|
|
214
|
+
scopes;
|
|
215
|
+
flowTimeoutMs;
|
|
216
|
+
clientId;
|
|
217
|
+
tokens;
|
|
218
|
+
loaded = false;
|
|
219
|
+
metadata;
|
|
220
|
+
inFlight;
|
|
221
|
+
constructor(opts) {
|
|
222
|
+
this.serverUrl = opts.serverUrl;
|
|
223
|
+
this.storage = opts.storage ?? new FileStorage;
|
|
224
|
+
this.storageKey = opts.storageKey;
|
|
225
|
+
this.redirectPort = opts.redirectPort ?? DEFAULT_PORT;
|
|
226
|
+
this.scopes = opts.scopes;
|
|
227
|
+
this.flowTimeoutMs = opts.flowTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
228
|
+
this.clientId = opts.clientId;
|
|
202
229
|
}
|
|
203
|
-
async
|
|
204
|
-
|
|
205
|
-
this.
|
|
206
|
-
|
|
230
|
+
async getHeader() {
|
|
231
|
+
await this.ensureTokens();
|
|
232
|
+
if (!this.tokens)
|
|
233
|
+
throw new Error("OAuth: no tokens after auth flow.");
|
|
234
|
+
return `Bearer ${this.tokens.accessToken}`;
|
|
207
235
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
# Skills
|
|
216
|
-
The following skills are available. When a task matches one, call the \`Skill\` tool with that skill's name to load its full content before proceeding.
|
|
217
|
-
|
|
218
|
-
${skillsList}`;
|
|
236
|
+
async onUnauthorized() {
|
|
237
|
+
if (this.tokens?.refreshToken) {
|
|
238
|
+
try {
|
|
239
|
+
await this.refresh();
|
|
240
|
+
return;
|
|
241
|
+
} catch {}
|
|
219
242
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
${list}`;
|
|
243
|
+
this.tokens = undefined;
|
|
244
|
+
await this.storage.clear(this.storageKey);
|
|
245
|
+
await this.authorize();
|
|
246
|
+
}
|
|
247
|
+
async ensureTokens() {
|
|
248
|
+
if (this.inFlight) {
|
|
249
|
+
await this.inFlight;
|
|
250
|
+
return;
|
|
229
251
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
252
|
+
this.inFlight = (async () => {
|
|
253
|
+
if (!this.loaded) {
|
|
254
|
+
this.tokens = await this.storage.load(this.storageKey) ?? undefined;
|
|
255
|
+
this.loaded = true;
|
|
256
|
+
}
|
|
257
|
+
if (!this.tokens) {
|
|
258
|
+
await this.authorize();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (this.isExpired(this.tokens)) {
|
|
262
|
+
if (this.tokens.refreshToken) {
|
|
263
|
+
try {
|
|
264
|
+
await this.refresh();
|
|
265
|
+
return;
|
|
266
|
+
} catch {}
|
|
267
|
+
}
|
|
268
|
+
await this.authorize();
|
|
269
|
+
}
|
|
270
|
+
})();
|
|
271
|
+
try {
|
|
272
|
+
await this.inFlight;
|
|
273
|
+
} finally {
|
|
274
|
+
this.inFlight = undefined;
|
|
234
275
|
}
|
|
235
|
-
return system;
|
|
236
276
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
277
|
+
isExpired(tokens) {
|
|
278
|
+
if (!tokens.expiresAt)
|
|
279
|
+
return false;
|
|
280
|
+
return Date.now() / 1000 >= tokens.expiresAt - 30;
|
|
281
|
+
}
|
|
282
|
+
async getMetadata() {
|
|
283
|
+
if (!this.metadata)
|
|
284
|
+
this.metadata = await discover(this.serverUrl);
|
|
285
|
+
return this.metadata;
|
|
286
|
+
}
|
|
287
|
+
async authorize() {
|
|
288
|
+
const meta = await this.getMetadata();
|
|
289
|
+
const redirectUri = this.tokens?.redirectUri ?? `http://127.0.0.1:${this.redirectPort}/callback`;
|
|
290
|
+
let clientId = this.tokens?.clientId ?? this.clientId;
|
|
291
|
+
if (!clientId) {
|
|
292
|
+
if (!meta.registration_endpoint) {
|
|
293
|
+
throw new Error("OAuth server does not support dynamic client registration; pass a clientId.");
|
|
254
294
|
}
|
|
295
|
+
clientId = await registerClient(meta.registration_endpoint, redirectUri);
|
|
296
|
+
}
|
|
297
|
+
const { verifier, challenge } = await generatePkce();
|
|
298
|
+
const state = generateState();
|
|
299
|
+
const port = parseInt(new URL(redirectUri).port, 10);
|
|
300
|
+
const codePromise = captureAuthorizationCode(port, state, this.flowTimeoutMs);
|
|
301
|
+
const authUrl = new URL(meta.authorization_endpoint);
|
|
302
|
+
authUrl.searchParams.set("response_type", "code");
|
|
303
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
304
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
305
|
+
authUrl.searchParams.set("code_challenge", challenge);
|
|
306
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
307
|
+
authUrl.searchParams.set("state", state);
|
|
308
|
+
if (this.scopes?.length) {
|
|
309
|
+
authUrl.searchParams.set("scope", this.scopes.join(" "));
|
|
310
|
+
}
|
|
311
|
+
console.error(`[mcp:oauth] Opening browser for ${this.storageKey} authentication...`);
|
|
312
|
+
openBrowser(authUrl.toString());
|
|
313
|
+
const code = await codePromise;
|
|
314
|
+
const response = await exchangeCode({
|
|
315
|
+
tokenEndpoint: meta.token_endpoint,
|
|
316
|
+
code,
|
|
317
|
+
clientId,
|
|
318
|
+
redirectUri,
|
|
319
|
+
verifier
|
|
255
320
|
});
|
|
321
|
+
this.tokens = this.toTokenSet(response, clientId, redirectUri);
|
|
322
|
+
await this.storage.save(this.storageKey, this.tokens);
|
|
256
323
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
];
|
|
279
|
-
registry = createRegistry(allTools);
|
|
280
|
-
ctx = {
|
|
281
|
-
complete: async (p) => {
|
|
282
|
-
const response = await this.model.createMessage({
|
|
283
|
-
messages: [{ role: "user", content: p }]
|
|
284
|
-
});
|
|
285
|
-
return response.content.filter((block) => block.type === "text").map((block) => block.text).join(`
|
|
286
|
-
`);
|
|
287
|
-
},
|
|
288
|
-
searchWeb: (query, opts) => this.model.searchWeb(query, opts),
|
|
289
|
-
session: state,
|
|
290
|
-
tools: registry,
|
|
291
|
-
cwd: sessionCwd
|
|
292
|
-
};
|
|
293
|
-
const systemMessage = {
|
|
294
|
-
role: "system",
|
|
295
|
-
content: this.buildSystem(allTools, systemExtension)
|
|
296
|
-
};
|
|
297
|
-
if (state.messages[0]?.role === "system") {
|
|
298
|
-
state.messages[0] = systemMessage;
|
|
299
|
-
} else {
|
|
300
|
-
state.messages.unshift(systemMessage);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return { tools: allTools, registry, ctx };
|
|
304
|
-
};
|
|
305
|
-
const internals = {
|
|
306
|
-
send: (prompt) => {
|
|
307
|
-
if (activeTurn !== null) {
|
|
308
|
-
throw new Error("Session.send() called while another turn is in flight.");
|
|
309
|
-
}
|
|
310
|
-
const turn = new Turn(async (turnCtx) => {
|
|
311
|
-
try {
|
|
312
|
-
const ready = await ensureReady();
|
|
313
|
-
state.messages.push(toUserMessage(prompt));
|
|
314
|
-
await runAgentLoop({
|
|
315
|
-
model: this.model,
|
|
316
|
-
state,
|
|
317
|
-
registry: ready.registry,
|
|
318
|
-
ctx: ready.ctx,
|
|
319
|
-
emit: turnCtx.emit,
|
|
320
|
-
signal: turnCtx.signal
|
|
321
|
-
});
|
|
322
|
-
return [...state.messages];
|
|
323
|
-
} finally {
|
|
324
|
-
activeTurn = null;
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
activeTurn = turn;
|
|
328
|
-
return turn;
|
|
329
|
-
},
|
|
330
|
-
close: async () => {
|
|
331
|
-
const pending = activeTurn;
|
|
332
|
-
if (pending) {
|
|
333
|
-
pending.abort("session closed");
|
|
334
|
-
await Promise.resolve(pending).catch(() => {});
|
|
335
|
-
}
|
|
336
|
-
if (mcpConnected) {
|
|
337
|
-
await Promise.all(this.mcp.map((mcp) => mcp.disconnect()));
|
|
338
|
-
mcpConnected = false;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
324
|
+
async refresh() {
|
|
325
|
+
if (!this.tokens?.refreshToken || !this.tokens.clientId) {
|
|
326
|
+
throw new Error("OAuth refresh: missing refresh token or client id.");
|
|
327
|
+
}
|
|
328
|
+
const meta = await this.getMetadata();
|
|
329
|
+
const response = await refreshTokens({
|
|
330
|
+
tokenEndpoint: meta.token_endpoint,
|
|
331
|
+
refreshToken: this.tokens.refreshToken,
|
|
332
|
+
clientId: this.tokens.clientId
|
|
333
|
+
});
|
|
334
|
+
this.tokens = this.toTokenSet(response, this.tokens.clientId, this.tokens.redirectUri, this.tokens.refreshToken);
|
|
335
|
+
await this.storage.save(this.storageKey, this.tokens);
|
|
336
|
+
}
|
|
337
|
+
toTokenSet(response, clientId, redirectUri, fallbackRefresh) {
|
|
338
|
+
const expiresAt = response.expires_in ? Math.floor(Date.now() / 1000) + response.expires_in : undefined;
|
|
339
|
+
return {
|
|
340
|
+
accessToken: response.access_token,
|
|
341
|
+
refreshToken: response.refresh_token ?? fallbackRefresh,
|
|
342
|
+
expiresAt,
|
|
343
|
+
clientId,
|
|
344
|
+
redirectUri
|
|
341
345
|
};
|
|
342
|
-
return new Session(internals, state);
|
|
343
346
|
}
|
|
344
347
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
break;
|
|
377
|
-
case "text-end":
|
|
378
|
-
assistantContent.push({ type: "text", text: event.text });
|
|
379
|
-
emit({ type: "text", text: event.text });
|
|
380
|
-
break;
|
|
381
|
-
case "thinking-delta":
|
|
382
|
-
emit({ type: "thinking-delta", text: event.text });
|
|
383
|
-
break;
|
|
384
|
-
case "thinking-end":
|
|
385
|
-
assistantContent.push({
|
|
386
|
-
type: "thinking",
|
|
387
|
-
text: event.text,
|
|
388
|
-
...event.signature !== undefined ? { signature: event.signature } : {},
|
|
389
|
-
...event.redactedData !== undefined ? { redactedData: event.redactedData } : {}
|
|
390
|
-
});
|
|
391
|
-
emit({
|
|
392
|
-
type: "thinking",
|
|
393
|
-
text: event.text,
|
|
394
|
-
...event.signature !== undefined ? { signature: event.signature } : {}
|
|
395
|
-
});
|
|
396
|
-
break;
|
|
397
|
-
case "tool-call":
|
|
398
|
-
assistantContent.push({
|
|
399
|
-
type: "tool-call",
|
|
400
|
-
toolCallId: event.toolCallId,
|
|
401
|
-
toolName: event.toolName,
|
|
402
|
-
input: event.input
|
|
403
|
-
});
|
|
404
|
-
emit({
|
|
405
|
-
type: "tool-call",
|
|
406
|
-
toolCallId: event.toolCallId,
|
|
407
|
-
toolName: event.toolName,
|
|
408
|
-
input: event.input
|
|
409
|
-
});
|
|
410
|
-
break;
|
|
411
|
-
case "message-end":
|
|
412
|
-
stopReason = event.stopReason;
|
|
413
|
-
usage = event.usage;
|
|
414
|
-
break;
|
|
415
|
-
default:
|
|
416
|
-
break;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
} catch (error) {
|
|
420
|
-
if (signal.aborted) {
|
|
421
|
-
state.messages.push({ role: "assistant", content: assistantContent });
|
|
422
|
-
break;
|
|
423
|
-
}
|
|
424
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
425
|
-
emit({ type: "error", error: err });
|
|
426
|
-
throw err;
|
|
427
|
-
}
|
|
428
|
-
state.messages.push({ role: "assistant", content: assistantContent });
|
|
429
|
-
emit({ type: "message-end", usage });
|
|
430
|
-
if (stopReason !== "tool_use") {
|
|
431
|
-
emit({ type: "turn-end" });
|
|
432
|
-
break;
|
|
348
|
+
|
|
349
|
+
// src/core/agent.ts
|
|
350
|
+
import { z } from "zod";
|
|
351
|
+
|
|
352
|
+
// src/core/session.ts
|
|
353
|
+
class Turn {
|
|
354
|
+
queue = [];
|
|
355
|
+
waiters = [];
|
|
356
|
+
ended = false;
|
|
357
|
+
iterated = false;
|
|
358
|
+
resultPromise;
|
|
359
|
+
abortController = new AbortController;
|
|
360
|
+
constructor(runner) {
|
|
361
|
+
this.resultPromise = runner({
|
|
362
|
+
emit: (event) => this.emit(event),
|
|
363
|
+
signal: this.abortController.signal
|
|
364
|
+
}).then((messages) => {
|
|
365
|
+
this.close();
|
|
366
|
+
return messages;
|
|
367
|
+
}, (error) => {
|
|
368
|
+
this.close();
|
|
369
|
+
throw error;
|
|
370
|
+
});
|
|
371
|
+
this.resultPromise.catch(() => {});
|
|
372
|
+
}
|
|
373
|
+
emit(event) {
|
|
374
|
+
const waiter = this.waiters.shift();
|
|
375
|
+
if (waiter) {
|
|
376
|
+
waiter({ value: event, done: false });
|
|
377
|
+
} else {
|
|
378
|
+
this.queue.push(event);
|
|
433
379
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const result = await runTool(tool, call, ctx);
|
|
441
|
-
results.push(result);
|
|
442
|
-
emit({
|
|
443
|
-
type: "tool-result",
|
|
444
|
-
toolCallId: result.toolCallId,
|
|
445
|
-
toolName: result.toolName,
|
|
446
|
-
output: result.output,
|
|
447
|
-
isError: typeof result.output === "string" ? result.output.startsWith("Error:") : false
|
|
448
|
-
});
|
|
380
|
+
}
|
|
381
|
+
close() {
|
|
382
|
+
this.ended = true;
|
|
383
|
+
while (this.waiters.length > 0) {
|
|
384
|
+
const waiter = this.waiters.shift();
|
|
385
|
+
waiter?.({ value: undefined, done: true });
|
|
449
386
|
}
|
|
450
|
-
state.messages.push({ role: "tool", content: results });
|
|
451
|
-
if (signal.aborted)
|
|
452
|
-
break;
|
|
453
387
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (!tool) {
|
|
457
|
-
return {
|
|
458
|
-
type: "tool-result",
|
|
459
|
-
toolCallId: call.toolCallId,
|
|
460
|
-
toolName: call.toolName,
|
|
461
|
-
output: `Error: tool "${call.toolName}" not found`
|
|
462
|
-
};
|
|
388
|
+
abort(reason) {
|
|
389
|
+
this.abortController.abort(reason);
|
|
463
390
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
toolCallId: call.toolCallId,
|
|
470
|
-
toolName: call.toolName,
|
|
471
|
-
output
|
|
472
|
-
};
|
|
473
|
-
} catch (error) {
|
|
391
|
+
[Symbol.asyncIterator]() {
|
|
392
|
+
if (this.iterated) {
|
|
393
|
+
throw new Error("Turn can only be iterated once.");
|
|
394
|
+
}
|
|
395
|
+
this.iterated = true;
|
|
474
396
|
return {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
case "thinking-end":
|
|
496
|
-
content.push({
|
|
497
|
-
type: "thinking",
|
|
498
|
-
text: event.text,
|
|
499
|
-
...event.signature !== undefined ? { signature: event.signature } : {},
|
|
500
|
-
...event.redactedData !== undefined ? { redactedData: event.redactedData } : {}
|
|
501
|
-
});
|
|
502
|
-
break;
|
|
503
|
-
case "tool-call":
|
|
504
|
-
content.push({
|
|
505
|
-
type: "tool-call",
|
|
506
|
-
toolCallId: event.toolCallId,
|
|
507
|
-
toolName: event.toolName,
|
|
508
|
-
input: event.input
|
|
509
|
-
});
|
|
510
|
-
break;
|
|
511
|
-
case "message-end":
|
|
512
|
-
id = event.id;
|
|
513
|
-
stopReason = event.stopReason;
|
|
514
|
-
usage = event.usage;
|
|
515
|
-
break;
|
|
516
|
-
default:
|
|
517
|
-
break;
|
|
397
|
+
next: () => {
|
|
398
|
+
if (this.queue.length > 0) {
|
|
399
|
+
const value = this.queue.shift();
|
|
400
|
+
return Promise.resolve({ value, done: false });
|
|
401
|
+
}
|
|
402
|
+
if (this.ended) {
|
|
403
|
+
return Promise.resolve({
|
|
404
|
+
value: undefined,
|
|
405
|
+
done: true
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
409
|
+
},
|
|
410
|
+
return: async () => {
|
|
411
|
+
this.abort("iteration ended");
|
|
412
|
+
await this.resultPromise.catch(() => {});
|
|
413
|
+
return {
|
|
414
|
+
value: undefined,
|
|
415
|
+
done: true
|
|
416
|
+
};
|
|
518
417
|
}
|
|
519
|
-
}
|
|
520
|
-
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
then(onfulfilled, onrejected) {
|
|
421
|
+
return this.resultPromise.then(onfulfilled, onrejected);
|
|
521
422
|
}
|
|
522
423
|
}
|
|
523
424
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
this.
|
|
536
|
-
|
|
425
|
+
class Session {
|
|
426
|
+
internals;
|
|
427
|
+
state;
|
|
428
|
+
constructor(internals, state) {
|
|
429
|
+
this.internals = internals;
|
|
430
|
+
this.state = state;
|
|
431
|
+
}
|
|
432
|
+
get messages() {
|
|
433
|
+
return this.state.messages;
|
|
434
|
+
}
|
|
435
|
+
send(prompt) {
|
|
436
|
+
return this.internals.send(prompt);
|
|
437
|
+
}
|
|
438
|
+
async close() {
|
|
439
|
+
await this.internals.close();
|
|
440
|
+
}
|
|
441
|
+
async[Symbol.asyncDispose]() {
|
|
442
|
+
await this.close();
|
|
537
443
|
}
|
|
538
444
|
}
|
|
539
445
|
|
|
540
|
-
// src/
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
str += String.fromCharCode(byte);
|
|
545
|
-
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
546
|
-
}
|
|
547
|
-
function randomBytes(length) {
|
|
548
|
-
const bytes = new Uint8Array(length);
|
|
549
|
-
crypto.getRandomValues(bytes);
|
|
550
|
-
return bytes;
|
|
551
|
-
}
|
|
552
|
-
async function sha256(input) {
|
|
553
|
-
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
554
|
-
return new Uint8Array(hash);
|
|
555
|
-
}
|
|
556
|
-
async function generatePkce() {
|
|
557
|
-
const verifier = base64UrlEncode(randomBytes(32));
|
|
558
|
-
const challenge = base64UrlEncode(await sha256(verifier));
|
|
559
|
-
return { verifier, challenge };
|
|
560
|
-
}
|
|
561
|
-
function generateState() {
|
|
562
|
-
return base64UrlEncode(randomBytes(16));
|
|
446
|
+
// src/core/tool.ts
|
|
447
|
+
import * as z4 from "zod/v4/core";
|
|
448
|
+
function hasParseMethod(schema) {
|
|
449
|
+
return typeof schema === "object" && schema !== null && "parse" in schema && typeof schema.parse === "function";
|
|
563
450
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
451
|
+
|
|
452
|
+
class Tool {
|
|
453
|
+
name;
|
|
454
|
+
description;
|
|
455
|
+
inputSchema;
|
|
456
|
+
jsonSchema;
|
|
457
|
+
execute;
|
|
458
|
+
deferred;
|
|
459
|
+
constructor(init) {
|
|
460
|
+
this.name = init.name;
|
|
461
|
+
this.description = init.description;
|
|
462
|
+
this.deferred = init.deferred ?? false;
|
|
463
|
+
if ("inputSchema" in init) {
|
|
464
|
+
this.inputSchema = init.inputSchema;
|
|
465
|
+
this.execute = init.execute;
|
|
466
|
+
} else {
|
|
467
|
+
this.jsonSchema = init.jsonSchema;
|
|
468
|
+
this.execute = init.execute;
|
|
574
469
|
}
|
|
575
|
-
} catch {}
|
|
576
|
-
const asUrl = new URL(authServer);
|
|
577
|
-
const asOrigin = `${asUrl.protocol}//${asUrl.host}`;
|
|
578
|
-
const res = await fetch(`${asOrigin}/.well-known/oauth-authorization-server`);
|
|
579
|
-
if (!res.ok) {
|
|
580
|
-
throw new Error(`OAuth discovery failed for ${asOrigin}: ${res.status} ${await res.text()}`);
|
|
581
470
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
throw new Error(`OAuth client registration failed: ${res.status} ${await res.text()}`);
|
|
471
|
+
toJsonSchema() {
|
|
472
|
+
if (this.jsonSchema !== undefined)
|
|
473
|
+
return this.jsonSchema;
|
|
474
|
+
if (this.inputSchema === undefined) {
|
|
475
|
+
throw new Error(`Tool "${this.name}" has neither inputSchema nor jsonSchema.`);
|
|
476
|
+
}
|
|
477
|
+
return z4.toJSONSchema(this.inputSchema);
|
|
478
|
+
}
|
|
479
|
+
parse(input) {
|
|
480
|
+
const schema = this.inputSchema;
|
|
481
|
+
if (!schema)
|
|
482
|
+
return input;
|
|
483
|
+
if (hasParseMethod(schema))
|
|
484
|
+
return schema.parse(input);
|
|
485
|
+
return z4.parse(schema, input);
|
|
598
486
|
}
|
|
599
|
-
const data = await res.json();
|
|
600
|
-
return data.client_id;
|
|
601
|
-
}
|
|
602
|
-
function openBrowser(url) {
|
|
603
|
-
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
604
|
-
Bun.spawn([cmd, url], { stdout: "ignore", stderr: "ignore" });
|
|
605
487
|
}
|
|
606
|
-
function
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
}
|
|
621
|
-
const code = u.searchParams.get("code");
|
|
622
|
-
const state = u.searchParams.get("state");
|
|
623
|
-
if (!code || state !== expectedState) {
|
|
624
|
-
finish(() => reject(new Error("OAuth state mismatch or missing code.")));
|
|
625
|
-
return new Response("Invalid OAuth response.", { status: 400 });
|
|
488
|
+
function createRegistry(tools) {
|
|
489
|
+
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
490
|
+
const loadedNames = new Set(tools.filter((tool) => !tool.deferred).map((tool) => tool.name));
|
|
491
|
+
return {
|
|
492
|
+
loaded: () => tools.filter((tool) => loadedNames.has(tool.name)),
|
|
493
|
+
deferred: () => tools.filter((tool) => tool.deferred && !loadedNames.has(tool.name)),
|
|
494
|
+
load: (names) => {
|
|
495
|
+
const loaded = [];
|
|
496
|
+
const missing = [];
|
|
497
|
+
for (const name of names) {
|
|
498
|
+
const tool = byName.get(name);
|
|
499
|
+
if (!tool) {
|
|
500
|
+
missing.push(name);
|
|
501
|
+
continue;
|
|
626
502
|
}
|
|
627
|
-
|
|
628
|
-
|
|
503
|
+
loadedNames.add(name);
|
|
504
|
+
loaded.push(tool);
|
|
629
505
|
}
|
|
630
|
-
|
|
631
|
-
const timer = setTimeout(() => {
|
|
632
|
-
finish(() => reject(new Error("OAuth flow timed out.")));
|
|
633
|
-
}, timeoutMs);
|
|
634
|
-
function finish(action) {
|
|
635
|
-
clearTimeout(timer);
|
|
636
|
-
setTimeout(() => server.stop(true), 50);
|
|
637
|
-
action();
|
|
506
|
+
return { loaded, missing };
|
|
638
507
|
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
async function exchangeCode(opts) {
|
|
642
|
-
const body = new URLSearchParams({
|
|
643
|
-
grant_type: "authorization_code",
|
|
644
|
-
code: opts.code,
|
|
645
|
-
redirect_uri: opts.redirectUri,
|
|
646
|
-
client_id: opts.clientId,
|
|
647
|
-
code_verifier: opts.verifier
|
|
648
|
-
});
|
|
649
|
-
const res = await fetch(opts.tokenEndpoint, {
|
|
650
|
-
method: "POST",
|
|
651
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
652
|
-
body
|
|
653
|
-
});
|
|
654
|
-
if (!res.ok) {
|
|
655
|
-
throw new Error(`OAuth token exchange failed: ${res.status} ${await res.text()}`);
|
|
656
|
-
}
|
|
657
|
-
return await res.json();
|
|
508
|
+
};
|
|
658
509
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
510
|
+
|
|
511
|
+
// src/core/agent.ts
|
|
512
|
+
class Agent {
|
|
513
|
+
model;
|
|
514
|
+
system;
|
|
515
|
+
tools;
|
|
516
|
+
skills;
|
|
517
|
+
mcp;
|
|
518
|
+
cwd;
|
|
519
|
+
registrations = [];
|
|
520
|
+
unsubs = [];
|
|
521
|
+
constructor({
|
|
522
|
+
model,
|
|
523
|
+
system,
|
|
524
|
+
tools,
|
|
525
|
+
skills,
|
|
526
|
+
mcp,
|
|
527
|
+
cwd
|
|
528
|
+
}) {
|
|
529
|
+
this.model = model;
|
|
530
|
+
this.system = system;
|
|
531
|
+
this.tools = tools;
|
|
532
|
+
this.skills = skills;
|
|
533
|
+
this.mcp = mcp ?? [];
|
|
534
|
+
this.cwd = cwd;
|
|
672
535
|
}
|
|
673
|
-
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// src/mcp/storage.ts
|
|
677
|
-
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
678
|
-
import { homedir } from "os";
|
|
679
|
-
import { dirname, join } from "path";
|
|
680
|
-
var DEFAULT_PATH = join(homedir(), ".roboport", "mcp-auth.json");
|
|
681
|
-
|
|
682
|
-
class FileStorage {
|
|
683
|
-
path;
|
|
684
|
-
cache;
|
|
685
|
-
constructor(path) {
|
|
686
|
-
this.path = path ?? DEFAULT_PATH;
|
|
536
|
+
on(trigger, handler) {
|
|
537
|
+
this.registrations.push({ trigger, handler });
|
|
687
538
|
}
|
|
688
|
-
async
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
539
|
+
async start() {
|
|
540
|
+
for (const { trigger, handler } of this.registrations) {
|
|
541
|
+
const unsub = await trigger.start((event) => {
|
|
542
|
+
Promise.resolve().then(() => handler(event)).catch((error) => {
|
|
543
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
544
|
+
console.error(`[roboport] trigger "${trigger.name}" handler failed: ${message}`);
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
this.unsubs.push(unsub);
|
|
696
548
|
}
|
|
697
|
-
return this.cache;
|
|
698
|
-
}
|
|
699
|
-
async write(data) {
|
|
700
|
-
await mkdir(dirname(this.path), { recursive: true });
|
|
701
|
-
await writeFile(this.path, JSON.stringify(data, null, 2), "utf8");
|
|
702
|
-
await chmod(this.path, 384);
|
|
703
|
-
this.cache = data;
|
|
704
|
-
}
|
|
705
|
-
async load(key) {
|
|
706
|
-
const data = await this.read();
|
|
707
|
-
return data[key] ?? null;
|
|
708
|
-
}
|
|
709
|
-
async save(key, tokens) {
|
|
710
|
-
const data = await this.read();
|
|
711
|
-
data[key] = tokens;
|
|
712
|
-
await this.write(data);
|
|
713
549
|
}
|
|
714
|
-
async
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
await
|
|
550
|
+
async stop() {
|
|
551
|
+
const unsubs = this.unsubs;
|
|
552
|
+
this.unsubs = [];
|
|
553
|
+
await Promise.all(unsubs.map((u) => u()));
|
|
718
554
|
}
|
|
719
|
-
|
|
555
|
+
buildSystem(allTools, systemExtension) {
|
|
556
|
+
let system = this.system;
|
|
557
|
+
if (this.skills.length > 0) {
|
|
558
|
+
const skillsList = this.skills.map((skill) => `- ${skill.name}: ${skill.description}`).join(`
|
|
559
|
+
`);
|
|
560
|
+
system = `${system}
|
|
720
561
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
async load(key) {
|
|
724
|
-
return this.data.get(key) ?? null;
|
|
725
|
-
}
|
|
726
|
-
async save(key, tokens) {
|
|
727
|
-
this.data.set(key, tokens);
|
|
728
|
-
}
|
|
729
|
-
async clear(key) {
|
|
730
|
-
this.data.delete(key);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
562
|
+
# Skills
|
|
563
|
+
The following skills are available. When a task matches one, call the \`Skill\` tool with that skill's name to load its full content before proceeding.
|
|
733
564
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
return `Bearer ${this.token}`;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
var DEFAULT_PORT = 33418;
|
|
745
|
-
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
565
|
+
${skillsList}`;
|
|
566
|
+
}
|
|
567
|
+
const deferred = allTools.filter((tool) => tool.deferred);
|
|
568
|
+
if (deferred.length > 0) {
|
|
569
|
+
const list = deferred.map((tool) => `- ${tool.name}`).join(`
|
|
570
|
+
`);
|
|
571
|
+
system = `${system}
|
|
746
572
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
storageKey;
|
|
751
|
-
redirectPort;
|
|
752
|
-
scopes;
|
|
753
|
-
flowTimeoutMs;
|
|
754
|
-
tokens;
|
|
755
|
-
loaded = false;
|
|
756
|
-
metadata;
|
|
757
|
-
inFlight;
|
|
758
|
-
constructor(opts) {
|
|
759
|
-
this.serverUrl = opts.serverUrl;
|
|
760
|
-
this.storage = opts.storage ?? new FileStorage;
|
|
761
|
-
this.storageKey = opts.storageKey;
|
|
762
|
-
this.redirectPort = opts.redirectPort ?? DEFAULT_PORT;
|
|
763
|
-
this.scopes = opts.scopes;
|
|
764
|
-
this.flowTimeoutMs = opts.flowTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
765
|
-
}
|
|
766
|
-
async getHeader() {
|
|
767
|
-
await this.ensureTokens();
|
|
768
|
-
if (!this.tokens)
|
|
769
|
-
throw new Error("OAuth: no tokens after auth flow.");
|
|
770
|
-
return `Bearer ${this.tokens.accessToken}`;
|
|
771
|
-
}
|
|
772
|
-
async onUnauthorized() {
|
|
773
|
-
if (this.tokens?.refreshToken) {
|
|
774
|
-
try {
|
|
775
|
-
await this.refresh();
|
|
776
|
-
return;
|
|
777
|
-
} catch {}
|
|
573
|
+
# Deferred tools
|
|
574
|
+
These tools are available but their schemas are not loaded. Use ToolSearch to load them before calling.
|
|
575
|
+
${list}`;
|
|
778
576
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
async ensureTokens() {
|
|
784
|
-
if (this.inFlight) {
|
|
785
|
-
await this.inFlight;
|
|
786
|
-
return;
|
|
577
|
+
if (systemExtension) {
|
|
578
|
+
system = `${system}
|
|
579
|
+
|
|
580
|
+
${systemExtension}`;
|
|
787
581
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
582
|
+
return system;
|
|
583
|
+
}
|
|
584
|
+
buildSkillTool() {
|
|
585
|
+
const byName = new Map(this.skills.map((skill) => [skill.name, skill]));
|
|
586
|
+
return new Tool({
|
|
587
|
+
name: "Skill",
|
|
588
|
+
description: 'Load the full content of a skill listed under "# Skills" in the system prompt. Call this when you decide a listed skill applies to the current task; the returned content extends your instructions for the rest of the session.',
|
|
589
|
+
inputSchema: z.object({
|
|
590
|
+
skill: z.string().describe("Name of the skill to load (must match a listed skill).")
|
|
591
|
+
}),
|
|
592
|
+
execute: ({ skill }) => {
|
|
593
|
+
const found = byName.get(skill);
|
|
594
|
+
if (!found) {
|
|
595
|
+
const available = [...byName.keys()].join(", ");
|
|
596
|
+
throw new Error(`Skill "${skill}" not found. Available: ${available}`);
|
|
597
|
+
}
|
|
598
|
+
return `<skill name="${found.name}">
|
|
599
|
+
${found.content}
|
|
600
|
+
</skill>`;
|
|
792
601
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
session(init) {
|
|
605
|
+
const initialMessages = init?.messages ? [...init.messages] : [];
|
|
606
|
+
const sessionCwd = init?.cwd ?? this.cwd ?? process.cwd();
|
|
607
|
+
const systemExtension = init?.systemExtension;
|
|
608
|
+
const state = {
|
|
609
|
+
messages: initialMessages,
|
|
610
|
+
store: new Map
|
|
611
|
+
};
|
|
612
|
+
let activeTurn = null;
|
|
613
|
+
let mcpConnected = false;
|
|
614
|
+
let allTools = null;
|
|
615
|
+
let registry = null;
|
|
616
|
+
let ctx = null;
|
|
617
|
+
const ensureReady = async () => {
|
|
618
|
+
if (!allTools || !registry || !ctx) {
|
|
619
|
+
const mcpToolGroups = await Promise.all(this.mcp.map((mcp) => mcp.connect()));
|
|
620
|
+
mcpConnected = true;
|
|
621
|
+
allTools = [
|
|
622
|
+
...this.tools,
|
|
623
|
+
...mcpToolGroups.flat(),
|
|
624
|
+
...this.skills.length > 0 ? [this.buildSkillTool()] : []
|
|
625
|
+
];
|
|
626
|
+
registry = createRegistry(allTools);
|
|
627
|
+
ctx = {
|
|
628
|
+
complete: async (p) => {
|
|
629
|
+
const response = await this.model.createMessage({
|
|
630
|
+
messages: [{ role: "user", content: p }]
|
|
631
|
+
});
|
|
632
|
+
return response.content.filter((block) => block.type === "text").map((block) => block.text).join(`
|
|
633
|
+
`);
|
|
634
|
+
},
|
|
635
|
+
searchWeb: (query, opts) => this.model.searchWeb(query, opts),
|
|
636
|
+
session: state,
|
|
637
|
+
tools: registry,
|
|
638
|
+
cwd: sessionCwd
|
|
639
|
+
};
|
|
640
|
+
const systemMessage = {
|
|
641
|
+
role: "system",
|
|
642
|
+
content: this.buildSystem(allTools, systemExtension)
|
|
643
|
+
};
|
|
644
|
+
if (state.messages[0]?.role === "system") {
|
|
645
|
+
state.messages[0] = systemMessage;
|
|
646
|
+
} else {
|
|
647
|
+
state.messages.unshift(systemMessage);
|
|
648
|
+
}
|
|
796
649
|
}
|
|
797
|
-
|
|
798
|
-
|
|
650
|
+
return { tools: allTools, registry, ctx };
|
|
651
|
+
};
|
|
652
|
+
const internals = {
|
|
653
|
+
send: (prompt) => {
|
|
654
|
+
if (activeTurn !== null) {
|
|
655
|
+
throw new Error("Session.send() called while another turn is in flight.");
|
|
656
|
+
}
|
|
657
|
+
const turn = new Turn(async (turnCtx) => {
|
|
799
658
|
try {
|
|
800
|
-
await
|
|
801
|
-
|
|
802
|
-
|
|
659
|
+
const ready = await ensureReady();
|
|
660
|
+
state.messages.push(toUserMessage(prompt));
|
|
661
|
+
await runAgentLoop({
|
|
662
|
+
model: this.model,
|
|
663
|
+
state,
|
|
664
|
+
registry: ready.registry,
|
|
665
|
+
ctx: ready.ctx,
|
|
666
|
+
emit: turnCtx.emit,
|
|
667
|
+
signal: turnCtx.signal
|
|
668
|
+
});
|
|
669
|
+
return [...state.messages];
|
|
670
|
+
} finally {
|
|
671
|
+
activeTurn = null;
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
activeTurn = turn;
|
|
675
|
+
return turn;
|
|
676
|
+
},
|
|
677
|
+
close: async () => {
|
|
678
|
+
const pending = activeTurn;
|
|
679
|
+
if (pending) {
|
|
680
|
+
pending.abort("session closed");
|
|
681
|
+
await Promise.resolve(pending).catch(() => {});
|
|
682
|
+
}
|
|
683
|
+
if (mcpConnected) {
|
|
684
|
+
await Promise.all(this.mcp.map((mcp) => mcp.disconnect()));
|
|
685
|
+
mcpConnected = false;
|
|
803
686
|
}
|
|
804
|
-
await this.authorize();
|
|
805
687
|
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
await this.inFlight;
|
|
809
|
-
} finally {
|
|
810
|
-
this.inFlight = undefined;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
isExpired(tokens) {
|
|
814
|
-
if (!tokens.expiresAt)
|
|
815
|
-
return false;
|
|
816
|
-
return Date.now() / 1000 >= tokens.expiresAt - 30;
|
|
817
|
-
}
|
|
818
|
-
async getMetadata() {
|
|
819
|
-
if (!this.metadata)
|
|
820
|
-
this.metadata = await discover(this.serverUrl);
|
|
821
|
-
return this.metadata;
|
|
688
|
+
};
|
|
689
|
+
return new Session(internals, state);
|
|
822
690
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
691
|
+
}
|
|
692
|
+
function toUserMessage(prompt) {
|
|
693
|
+
if (typeof prompt === "string")
|
|
694
|
+
return { role: "user", content: prompt };
|
|
695
|
+
return { role: "user", content: prompt };
|
|
696
|
+
}
|
|
697
|
+
async function runAgentLoop({
|
|
698
|
+
model,
|
|
699
|
+
state,
|
|
700
|
+
registry,
|
|
701
|
+
ctx,
|
|
702
|
+
emit,
|
|
703
|
+
signal
|
|
704
|
+
}) {
|
|
705
|
+
while (true) {
|
|
706
|
+
if (signal.aborted)
|
|
707
|
+
break;
|
|
708
|
+
const active = registry.loaded();
|
|
709
|
+
const toolByName = new Map(active.map((tool) => [tool.name, tool]));
|
|
710
|
+
emit({ type: "message-start" });
|
|
711
|
+
const assistantContent = [];
|
|
712
|
+
let stopReason = "end_turn";
|
|
713
|
+
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
714
|
+
try {
|
|
715
|
+
for await (const event of model.streamMessage({
|
|
716
|
+
messages: state.messages,
|
|
717
|
+
tools: active,
|
|
718
|
+
signal
|
|
719
|
+
})) {
|
|
720
|
+
switch (event.type) {
|
|
721
|
+
case "text-delta":
|
|
722
|
+
emit({ type: "text-delta", text: event.text });
|
|
723
|
+
break;
|
|
724
|
+
case "text-end":
|
|
725
|
+
assistantContent.push({ type: "text", text: event.text });
|
|
726
|
+
emit({ type: "text", text: event.text });
|
|
727
|
+
break;
|
|
728
|
+
case "thinking-delta":
|
|
729
|
+
emit({ type: "thinking-delta", text: event.text });
|
|
730
|
+
break;
|
|
731
|
+
case "thinking-end":
|
|
732
|
+
assistantContent.push({
|
|
733
|
+
type: "thinking",
|
|
734
|
+
text: event.text,
|
|
735
|
+
...event.signature !== undefined ? { signature: event.signature } : {},
|
|
736
|
+
...event.redactedData !== undefined ? { redactedData: event.redactedData } : {}
|
|
737
|
+
});
|
|
738
|
+
emit({
|
|
739
|
+
type: "thinking",
|
|
740
|
+
text: event.text,
|
|
741
|
+
...event.signature !== undefined ? { signature: event.signature } : {}
|
|
742
|
+
});
|
|
743
|
+
break;
|
|
744
|
+
case "tool-call":
|
|
745
|
+
assistantContent.push({
|
|
746
|
+
type: "tool-call",
|
|
747
|
+
toolCallId: event.toolCallId,
|
|
748
|
+
toolName: event.toolName,
|
|
749
|
+
input: event.input
|
|
750
|
+
});
|
|
751
|
+
emit({
|
|
752
|
+
type: "tool-call",
|
|
753
|
+
toolCallId: event.toolCallId,
|
|
754
|
+
toolName: event.toolName,
|
|
755
|
+
input: event.input
|
|
756
|
+
});
|
|
757
|
+
break;
|
|
758
|
+
case "message-end":
|
|
759
|
+
stopReason = event.stopReason;
|
|
760
|
+
usage = event.usage;
|
|
761
|
+
break;
|
|
762
|
+
default:
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
830
765
|
}
|
|
831
|
-
|
|
766
|
+
} catch (error) {
|
|
767
|
+
if (signal.aborted) {
|
|
768
|
+
state.messages.push({ role: "assistant", content: assistantContent });
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
772
|
+
emit({ type: "error", error: err });
|
|
773
|
+
throw err;
|
|
832
774
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
authUrl.searchParams.set("response_type", "code");
|
|
839
|
-
authUrl.searchParams.set("client_id", clientId);
|
|
840
|
-
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
841
|
-
authUrl.searchParams.set("code_challenge", challenge);
|
|
842
|
-
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
843
|
-
authUrl.searchParams.set("state", state);
|
|
844
|
-
if (this.scopes?.length) {
|
|
845
|
-
authUrl.searchParams.set("scope", this.scopes.join(" "));
|
|
775
|
+
state.messages.push({ role: "assistant", content: assistantContent });
|
|
776
|
+
emit({ type: "message-end", usage });
|
|
777
|
+
if (stopReason !== "tool_use") {
|
|
778
|
+
emit({ type: "turn-end" });
|
|
779
|
+
break;
|
|
846
780
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
throw new Error("OAuth refresh: missing refresh token or client id.");
|
|
781
|
+
const toolCalls = assistantContent.filter((block) => block.type === "tool-call");
|
|
782
|
+
const results = [];
|
|
783
|
+
for (const call of toolCalls) {
|
|
784
|
+
if (signal.aborted)
|
|
785
|
+
break;
|
|
786
|
+
const tool = toolByName.get(call.toolName);
|
|
787
|
+
const result = await runTool(tool, call, ctx);
|
|
788
|
+
results.push(result);
|
|
789
|
+
emit({
|
|
790
|
+
type: "tool-result",
|
|
791
|
+
toolCallId: result.toolCallId,
|
|
792
|
+
toolName: result.toolName,
|
|
793
|
+
output: result.output,
|
|
794
|
+
isError: typeof result.output === "string" ? result.output.startsWith("Error:") : false
|
|
795
|
+
});
|
|
863
796
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
refreshToken: this.tokens.refreshToken,
|
|
868
|
-
clientId: this.tokens.clientId
|
|
869
|
-
});
|
|
870
|
-
this.tokens = this.toTokenSet(response, this.tokens.clientId, this.tokens.redirectUri, this.tokens.refreshToken);
|
|
871
|
-
await this.storage.save(this.storageKey, this.tokens);
|
|
797
|
+
state.messages.push({ role: "tool", content: results });
|
|
798
|
+
if (signal.aborted)
|
|
799
|
+
break;
|
|
872
800
|
}
|
|
873
|
-
|
|
874
|
-
|
|
801
|
+
}
|
|
802
|
+
async function runTool(tool, call, ctx) {
|
|
803
|
+
if (!tool) {
|
|
875
804
|
return {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
805
|
+
type: "tool-result",
|
|
806
|
+
toolCallId: call.toolCallId,
|
|
807
|
+
toolName: call.toolName,
|
|
808
|
+
output: `Error: tool "${call.toolName}" not found`
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
try {
|
|
812
|
+
const parsed = tool.parse(call.input);
|
|
813
|
+
const output = await tool.execute(parsed, ctx);
|
|
814
|
+
return {
|
|
815
|
+
type: "tool-result",
|
|
816
|
+
toolCallId: call.toolCallId,
|
|
817
|
+
toolName: call.toolName,
|
|
818
|
+
output
|
|
819
|
+
};
|
|
820
|
+
} catch (error) {
|
|
821
|
+
return {
|
|
822
|
+
type: "tool-result",
|
|
823
|
+
toolCallId: call.toolCallId,
|
|
824
|
+
toolName: call.toolName,
|
|
825
|
+
output: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
881
826
|
};
|
|
882
827
|
}
|
|
883
828
|
}
|
|
884
829
|
|
|
830
|
+
// src/core/model.ts
|
|
831
|
+
class Model {
|
|
832
|
+
async createMessage(params) {
|
|
833
|
+
const content = [];
|
|
834
|
+
let id = "";
|
|
835
|
+
let stopReason = "end_turn";
|
|
836
|
+
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
837
|
+
for await (const event of this.streamMessage(params)) {
|
|
838
|
+
switch (event.type) {
|
|
839
|
+
case "text-end":
|
|
840
|
+
content.push({ type: "text", text: event.text });
|
|
841
|
+
break;
|
|
842
|
+
case "thinking-end":
|
|
843
|
+
content.push({
|
|
844
|
+
type: "thinking",
|
|
845
|
+
text: event.text,
|
|
846
|
+
...event.signature !== undefined ? { signature: event.signature } : {},
|
|
847
|
+
...event.redactedData !== undefined ? { redactedData: event.redactedData } : {}
|
|
848
|
+
});
|
|
849
|
+
break;
|
|
850
|
+
case "tool-call":
|
|
851
|
+
content.push({
|
|
852
|
+
type: "tool-call",
|
|
853
|
+
toolCallId: event.toolCallId,
|
|
854
|
+
toolName: event.toolName,
|
|
855
|
+
input: event.input
|
|
856
|
+
});
|
|
857
|
+
break;
|
|
858
|
+
case "message-end":
|
|
859
|
+
id = event.id;
|
|
860
|
+
stopReason = event.stopReason;
|
|
861
|
+
usage = event.usage;
|
|
862
|
+
break;
|
|
863
|
+
default:
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return { id, content, stopReason, usage };
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/core/skill.ts
|
|
872
|
+
class Skill {
|
|
873
|
+
name;
|
|
874
|
+
description;
|
|
875
|
+
content;
|
|
876
|
+
constructor({
|
|
877
|
+
name,
|
|
878
|
+
description,
|
|
879
|
+
content
|
|
880
|
+
}) {
|
|
881
|
+
this.name = name;
|
|
882
|
+
this.description = description;
|
|
883
|
+
this.content = content;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
885
887
|
// src/mcp/clients/grafana.ts
|
|
886
888
|
var EMPTY_OBJECT = {
|
|
887
889
|
type: "object",
|
|
@@ -1272,6 +1274,9 @@ class Mcp2 {
|
|
|
1272
1274
|
transport,
|
|
1273
1275
|
deferred
|
|
1274
1276
|
}) {
|
|
1277
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
1278
|
+
throw new Error(`Invalid MCP name "${name}": use only letters, digits, underscores, and hyphens.`);
|
|
1279
|
+
}
|
|
1275
1280
|
this.name = name;
|
|
1276
1281
|
this.transport = transport;
|
|
1277
1282
|
this.deferred = deferred ?? true;
|
|
@@ -1360,6 +1365,11 @@ class Mcp4 extends Mcp2 {
|
|
|
1360
1365
|
var tenderly_default = Mcp4;
|
|
1361
1366
|
export {
|
|
1362
1367
|
tenderly_default as Tenderly,
|
|
1368
|
+
OAuthAuth,
|
|
1369
|
+
MemoryStorage,
|
|
1370
|
+
Mcp2 as Mcp,
|
|
1363
1371
|
linear_default as Linear,
|
|
1364
|
-
grafana_default as Grafana
|
|
1372
|
+
grafana_default as Grafana,
|
|
1373
|
+
FileStorage,
|
|
1374
|
+
BearerAuth
|
|
1365
1375
|
};
|