@ottocode/server 0.1.200 → 0.1.201
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.201",
|
|
4
4
|
"description": "HTTP API server for ottocode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"typecheck": "tsc --noEmit"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@ottocode/sdk": "0.1.
|
|
53
|
-
"@ottocode/database": "0.1.
|
|
52
|
+
"@ottocode/sdk": "0.1.201",
|
|
53
|
+
"@ottocode/database": "0.1.201",
|
|
54
54
|
"drizzle-orm": "^0.44.5",
|
|
55
55
|
"hono": "^4.9.9",
|
|
56
56
|
"zod": "^4.1.8"
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { registerSessionApprovalRoute } from './routes/session-approval.ts';
|
|
|
20
20
|
import { registerSetuRoutes } from './routes/setu.ts';
|
|
21
21
|
import { registerAuthRoutes } from './routes/auth.ts';
|
|
22
22
|
import { registerTunnelRoutes } from './routes/tunnel.ts';
|
|
23
|
+
import { registerMCPRoutes } from './routes/mcp.ts';
|
|
23
24
|
import { registerProviderUsageRoutes } from './routes/provider-usage.ts';
|
|
24
25
|
import type { AgentConfigEntry } from './runtime/agent/registry.ts';
|
|
25
26
|
|
|
@@ -77,6 +78,7 @@ function initApp() {
|
|
|
77
78
|
registerSetuRoutes(app);
|
|
78
79
|
registerAuthRoutes(app);
|
|
79
80
|
registerTunnelRoutes(app);
|
|
81
|
+
registerMCPRoutes(app);
|
|
80
82
|
registerProviderUsageRoutes(app);
|
|
81
83
|
|
|
82
84
|
return app;
|
|
@@ -150,6 +152,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
|
|
|
150
152
|
registerSetuRoutes(honoApp);
|
|
151
153
|
registerAuthRoutes(honoApp);
|
|
152
154
|
registerTunnelRoutes(honoApp);
|
|
155
|
+
registerMCPRoutes(honoApp);
|
|
153
156
|
registerProviderUsageRoutes(honoApp);
|
|
154
157
|
|
|
155
158
|
return honoApp;
|
|
@@ -251,6 +254,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
|
|
|
251
254
|
registerSetuRoutes(honoApp);
|
|
252
255
|
registerAuthRoutes(honoApp);
|
|
253
256
|
registerTunnelRoutes(honoApp);
|
|
257
|
+
registerMCPRoutes(honoApp);
|
|
254
258
|
registerProviderUsageRoutes(honoApp);
|
|
255
259
|
|
|
256
260
|
return honoApp;
|
package/src/presets.ts
CHANGED
|
@@ -18,7 +18,6 @@ export const BUILTIN_AGENTS = {
|
|
|
18
18
|
'tree',
|
|
19
19
|
'bash',
|
|
20
20
|
'update_todos',
|
|
21
|
-
'grep',
|
|
22
21
|
'terminal',
|
|
23
22
|
'git_status',
|
|
24
23
|
'git_diff',
|
|
@@ -67,7 +66,6 @@ export const BUILTIN_TOOLS = [
|
|
|
67
66
|
'tree',
|
|
68
67
|
'bash',
|
|
69
68
|
'terminal',
|
|
70
|
-
'grep',
|
|
71
69
|
'ripgrep',
|
|
72
70
|
'git_status',
|
|
73
71
|
'git_diff',
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import {
|
|
3
|
+
getMCPManager,
|
|
4
|
+
initializeMCP,
|
|
5
|
+
loadMCPConfig,
|
|
6
|
+
getGlobalConfigDir,
|
|
7
|
+
MCPClientWrapper,
|
|
8
|
+
addMCPServerToConfig,
|
|
9
|
+
removeMCPServerFromConfig,
|
|
10
|
+
} from '@ottocode/sdk';
|
|
11
|
+
|
|
12
|
+
export function registerMCPRoutes(app: Hono) {
|
|
13
|
+
app.get('/v1/mcp/servers', async (c) => {
|
|
14
|
+
const projectRoot = process.cwd();
|
|
15
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
16
|
+
const manager = getMCPManager();
|
|
17
|
+
const statuses = manager ? await manager.getStatusAsync() : [];
|
|
18
|
+
|
|
19
|
+
const servers = config.servers.map((s) => {
|
|
20
|
+
const status = statuses.find((st) => st.name === s.name);
|
|
21
|
+
return {
|
|
22
|
+
name: s.name,
|
|
23
|
+
transport: s.transport ?? 'stdio',
|
|
24
|
+
command: s.command,
|
|
25
|
+
args: s.args ?? [],
|
|
26
|
+
url: s.url,
|
|
27
|
+
disabled: s.disabled ?? false,
|
|
28
|
+
connected: status?.connected ?? false,
|
|
29
|
+
tools: status?.tools ?? [],
|
|
30
|
+
authRequired: status?.authRequired ?? false,
|
|
31
|
+
authenticated: status?.authenticated ?? false,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return c.json({ servers });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
app.post('/v1/mcp/servers', async (c) => {
|
|
39
|
+
const projectRoot = process.cwd();
|
|
40
|
+
const body = await c.req.json();
|
|
41
|
+
|
|
42
|
+
const { name, transport, command, args, env, url, headers, oauth } = body;
|
|
43
|
+
if (!name) {
|
|
44
|
+
return c.json({ ok: false, error: 'name is required' }, 400);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const t = transport ?? 'stdio';
|
|
48
|
+
if (t === 'stdio' && !command) {
|
|
49
|
+
return c.json(
|
|
50
|
+
{ ok: false, error: 'command is required for stdio transport' },
|
|
51
|
+
400,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if ((t === 'http' || t === 'sse') && !url) {
|
|
55
|
+
return c.json(
|
|
56
|
+
{ ok: false, error: 'url is required for http/sse transport' },
|
|
57
|
+
400,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const serverConfig = {
|
|
62
|
+
name: String(name),
|
|
63
|
+
transport: t,
|
|
64
|
+
...(command ? { command: String(command) } : {}),
|
|
65
|
+
...(Array.isArray(args) ? { args: args.map(String) } : {}),
|
|
66
|
+
...(env && typeof env === 'object' ? { env } : {}),
|
|
67
|
+
...(url ? { url: String(url) } : {}),
|
|
68
|
+
...(headers && typeof headers === 'object' ? { headers } : {}),
|
|
69
|
+
...(oauth && typeof oauth === 'object' ? { oauth } : {}),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await addMCPServerToConfig(projectRoot, serverConfig);
|
|
74
|
+
return c.json({ ok: true, server: serverConfig });
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
77
|
+
return c.json({ ok: false, error: msg }, 500);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
app.delete('/v1/mcp/servers/:name', async (c) => {
|
|
82
|
+
const name = c.req.param('name');
|
|
83
|
+
const projectRoot = process.cwd();
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const manager = getMCPManager();
|
|
87
|
+
if (manager) {
|
|
88
|
+
await manager.stopServer(name);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const removed = await removeMCPServerFromConfig(projectRoot, name);
|
|
92
|
+
if (!removed) {
|
|
93
|
+
return c.json({ ok: false, error: `Server "${name}" not found` }, 404);
|
|
94
|
+
}
|
|
95
|
+
return c.json({ ok: true, name });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
98
|
+
return c.json({ ok: false, error: msg }, 500);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
app.post('/v1/mcp/servers/:name/start', async (c) => {
|
|
103
|
+
const name = c.req.param('name');
|
|
104
|
+
const projectRoot = process.cwd();
|
|
105
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
106
|
+
const serverConfig = config.servers.find((s) => s.name === name);
|
|
107
|
+
|
|
108
|
+
if (!serverConfig) {
|
|
109
|
+
return c.json({ ok: false, error: `Server "${name}" not found` }, 404);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
let manager = getMCPManager();
|
|
114
|
+
if (!manager) {
|
|
115
|
+
manager = await initializeMCP({ servers: [] });
|
|
116
|
+
}
|
|
117
|
+
await manager.restartServer(serverConfig);
|
|
118
|
+
const status = (await manager.getStatusAsync()).find(
|
|
119
|
+
(s) => s.name === name,
|
|
120
|
+
);
|
|
121
|
+
return c.json({
|
|
122
|
+
ok: true,
|
|
123
|
+
name,
|
|
124
|
+
connected: status?.connected ?? false,
|
|
125
|
+
tools: status?.tools ?? [],
|
|
126
|
+
authRequired: status?.authRequired ?? false,
|
|
127
|
+
authUrl: manager.getAuthUrl(name),
|
|
128
|
+
});
|
|
129
|
+
} catch (err) {
|
|
130
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
131
|
+
return c.json({ ok: false, error: msg }, 500);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
app.post('/v1/mcp/servers/:name/stop', async (c) => {
|
|
136
|
+
const name = c.req.param('name');
|
|
137
|
+
const manager = getMCPManager();
|
|
138
|
+
|
|
139
|
+
if (!manager) {
|
|
140
|
+
return c.json({ ok: false, error: 'No MCP manager active' }, 400);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await manager.stopServer(name);
|
|
145
|
+
return c.json({ ok: true, name, connected: false });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
148
|
+
return c.json({ ok: false, error: msg }, 500);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
app.post('/v1/mcp/servers/:name/auth', async (c) => {
|
|
153
|
+
const name = c.req.param('name');
|
|
154
|
+
const projectRoot = process.cwd();
|
|
155
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
156
|
+
const serverConfig = config.servers.find((s) => s.name === name);
|
|
157
|
+
|
|
158
|
+
if (!serverConfig) {
|
|
159
|
+
return c.json({ ok: false, error: `Server "${name}" not found` }, 404);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
let manager = getMCPManager();
|
|
164
|
+
if (!manager) {
|
|
165
|
+
manager = await initializeMCP({ servers: [] });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const authUrl = await manager.initiateAuth(serverConfig);
|
|
169
|
+
if (authUrl) {
|
|
170
|
+
return c.json({ ok: true, authUrl, name });
|
|
171
|
+
}
|
|
172
|
+
return c.json({
|
|
173
|
+
ok: true,
|
|
174
|
+
name,
|
|
175
|
+
message: 'Already authenticated or no auth required',
|
|
176
|
+
});
|
|
177
|
+
} catch (err) {
|
|
178
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
179
|
+
return c.json({ ok: false, error: msg }, 500);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
app.post('/v1/mcp/servers/:name/auth/callback', async (c) => {
|
|
184
|
+
const name = c.req.param('name');
|
|
185
|
+
const body = await c.req.json();
|
|
186
|
+
const { code } = body;
|
|
187
|
+
|
|
188
|
+
if (!code) {
|
|
189
|
+
return c.json({ ok: false, error: 'code is required' }, 400);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const manager = getMCPManager();
|
|
193
|
+
if (!manager) {
|
|
194
|
+
return c.json({ ok: false, error: 'No MCP manager active' }, 400);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const success = await manager.completeAuth(name, String(code));
|
|
199
|
+
if (success) {
|
|
200
|
+
const status = (await manager.getStatusAsync()).find(
|
|
201
|
+
(s) => s.name === name,
|
|
202
|
+
);
|
|
203
|
+
return c.json({
|
|
204
|
+
ok: true,
|
|
205
|
+
name,
|
|
206
|
+
connected: status?.connected ?? false,
|
|
207
|
+
tools: status?.tools ?? [],
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return c.json({ ok: false, error: 'Auth completion failed' }, 500);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
213
|
+
return c.json({ ok: false, error: msg }, 500);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
app.get('/v1/mcp/servers/:name/auth/status', async (c) => {
|
|
218
|
+
const name = c.req.param('name');
|
|
219
|
+
const manager = getMCPManager();
|
|
220
|
+
|
|
221
|
+
if (!manager) {
|
|
222
|
+
return c.json({ authenticated: false });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const status = await manager.getAuthStatus(name);
|
|
227
|
+
return c.json(status);
|
|
228
|
+
} catch {
|
|
229
|
+
return c.json({ authenticated: false });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
app.delete('/v1/mcp/servers/:name/auth', async (c) => {
|
|
234
|
+
const name = c.req.param('name');
|
|
235
|
+
const manager = getMCPManager();
|
|
236
|
+
|
|
237
|
+
if (!manager) {
|
|
238
|
+
return c.json({ ok: false, error: 'No MCP manager active' }, 400);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
await manager.revokeAuth(name);
|
|
243
|
+
return c.json({ ok: true, name });
|
|
244
|
+
} catch (err) {
|
|
245
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
246
|
+
return c.json({ ok: false, error: msg }, 500);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
app.post('/v1/mcp/servers/:name/test', async (c) => {
|
|
251
|
+
const name = c.req.param('name');
|
|
252
|
+
const projectRoot = process.cwd();
|
|
253
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
254
|
+
const serverConfig = config.servers.find((s) => s.name === name);
|
|
255
|
+
|
|
256
|
+
if (!serverConfig) {
|
|
257
|
+
return c.json({ ok: false, error: `Server "${name}" not found` }, 404);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const client = new MCPClientWrapper(serverConfig);
|
|
261
|
+
try {
|
|
262
|
+
await client.connect();
|
|
263
|
+
const tools = await client.listTools();
|
|
264
|
+
await client.disconnect();
|
|
265
|
+
return c.json({
|
|
266
|
+
ok: true,
|
|
267
|
+
name,
|
|
268
|
+
tools: tools.map((t) => ({
|
|
269
|
+
name: t.name,
|
|
270
|
+
description: t.description,
|
|
271
|
+
})),
|
|
272
|
+
});
|
|
273
|
+
} catch (err) {
|
|
274
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
275
|
+
return c.json({ ok: false, error: msg }, 500);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
@@ -160,8 +160,10 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
160
160
|
|
|
161
161
|
toolsTimer.end({ count: allTools.length });
|
|
162
162
|
const allowedNames = new Set([...(agentCfg.tools || []), 'finish']);
|
|
163
|
-
const gated = allTools.filter(
|
|
164
|
-
|
|
163
|
+
const gated = allTools.filter(
|
|
164
|
+
(tool) => allowedNames.has(tool.name) || tool.name.includes('__'),
|
|
165
|
+
);
|
|
166
|
+
debugLog(`[tools] ${gated.length} allowed tools (including MCP)`);
|
|
165
167
|
|
|
166
168
|
debugLog(`[RUNNER] About to create model with provider: ${opts.provider}`);
|
|
167
169
|
debugLog(`[RUNNER] About to create model ID: ${opts.model}`);
|