@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.200",
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.200",
53
- "@ottocode/database": "0.1.200",
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((tool) => allowedNames.has(tool.name));
164
- debugLog(`[tools] ${gated.length} allowed tools`);
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}`);
@@ -71,7 +71,6 @@ function describeToolResult(info: ToolResultInfo): TargetDescriptor | null {
71
71
  case 'read':
72
72
  return describeRead(info);
73
73
  case 'glob':
74
- case 'grep':
75
74
  return describePatternTool(info, toolName);
76
75
  case 'write':
77
76
  return describeWrite(info);
@@ -25,8 +25,7 @@ export const CANONICAL_TO_PASCAL: Record<string, string> = {
25
25
 
26
26
  // Search operations
27
27
  glob: 'Glob',
28
- ripgrep: 'Grep', // Maps to Grep for Claude Code compatibility
29
- grep: 'Grep',
28
+ ripgrep: 'Grep',
30
29
 
31
30
  // Execution
32
31
  bash: 'Bash',