@loicngr/kobo 1.8.5 → 1.8.6
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/CHANGELOG.md +4 -0
- package/dist/mcp-server/kobo-tasks-handlers.js +18 -0
- package/dist/mcp-server/kobo-tasks-server.js +458 -297
- package/dist/server/db/migrations.js +16 -0
- package/dist/server/db/schema.js +1 -0
- package/dist/server/routes/workspaces.js +2 -1
- package/dist/server/services/auto-loop-service.js +17 -4
- package/dist/server/services/forge/types.js +9 -4
- package/package.json +6 -2
- package/src/client/dist/spa/assets/{ActivityFeed-NeOuB4hK.js → ActivityFeed-Cru3Y4vS.js} +1 -1
- package/src/client/dist/spa/assets/{ChangelogPage-DYhK35Uj.js → ChangelogPage-Bg-DDspc.js} +1 -1
- package/src/client/dist/spa/assets/{ClosePopup-SUgOzeRa.js → ClosePopup-HRef5kSi.js} +1 -1
- package/src/client/dist/spa/assets/CreatePage-C-2rBnTd.css +1 -0
- package/src/client/dist/spa/assets/CreatePage-DqZTSsVk.js +2 -0
- package/src/client/dist/spa/assets/DiffViewer-D_yY4zph.js +8 -0
- package/src/client/dist/spa/assets/{HealthPage-CcYmw0qz.js → HealthPage-DuOcnPX7.js} +1 -1
- package/src/client/dist/spa/assets/{MainLayout-E8V_3jtB.js → MainLayout-C4VC4R3n.js} +3 -3
- package/src/client/dist/spa/assets/QBtnGroup-CLMT4pWA.js +1 -0
- package/src/client/dist/spa/assets/QBtnToggle-Bnfx8-LH.js +1 -0
- package/src/client/dist/spa/assets/{QChip-C_hQ8zDG.js → QChip-UP-CYKPe.js} +1 -1
- package/src/client/dist/spa/assets/{QExpansionItem-CDMqMrBW.js → QExpansionItem-BGl-iIpb.js} +1 -1
- package/src/client/dist/spa/assets/{QScrollArea-5SknWwXm.js → QScrollArea-BsnAIb_6.js} +1 -1
- package/src/client/dist/spa/assets/{QSelect-DdFxTwDW.js → QSelect-DHWr1DWW.js} +1 -1
- package/src/client/dist/spa/assets/{QTooltip-wdRYvb7_.js → QTooltip-1Cqe-wzt.js} +1 -1
- package/src/client/dist/spa/assets/{SearchPage-CHcHBahe.js → SearchPage-CQ_Rw4cC.js} +1 -1
- package/src/client/dist/spa/assets/{SettingsPage-D4WRKgef.js → SettingsPage-Xt75bNBF.js} +1 -1
- package/src/client/dist/spa/assets/{TouchPan-CGfbFZ29.js → TouchPan-8v88x-k5.js} +1 -1
- package/src/client/dist/spa/assets/{WorkspacePage-ZMocf3BW.js → WorkspacePage-n6XL6ra6.js} +3 -3
- package/src/client/dist/spa/assets/build-path-tree-CsEz_AvN.js +1 -0
- package/src/client/dist/spa/assets/{cssMode-DtrJq-En.js → cssMode-DaW1C4P4.js} +1 -1
- package/src/client/dist/spa/assets/{documents-BbpzccXz.js → documents-DsG4t5fl.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api2-CinEgB5d.js → editor.api2-Bjz6ygpj.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-BRcMV3rU.js → editor.main-CLBGHEET.js} +2 -2
- package/src/client/dist/spa/assets/{expand-template-BT_OMXXZ.js → expand-template-BtZ2WHZx.js} +1 -1
- package/src/client/dist/spa/assets/{formatters-D24DKtIv.js → formatters-CzrIaq3v.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-CjI8M1Pj.js → freemarker2-CQz_-RAv.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-BrrbROQJ.js → handlebars-D3T_RE92.js} +1 -1
- package/src/client/dist/spa/assets/{html-Dt61THwD.js → html-C19hlQ10.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-tb4PrwZE.js → htmlMode-CcKHMqav.js} +1 -1
- package/src/client/dist/spa/assets/i18n-gU9Nz2Dy.js +1 -0
- package/src/client/dist/spa/assets/{index-DY-0-pzs.js → index-C1u3FMCQ.js} +6 -6
- package/src/client/dist/spa/assets/{javascript-CbNLIl6K.js → javascript-BtbPTi_2.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-9OB1NWBc.js → jsonMode-M6yQyl3x.js} +1 -1
- package/src/client/dist/spa/assets/{kobo-commands-BAFoc4XT.js → kobo-commands-CRSnRjVe.js} +1 -1
- package/src/client/dist/spa/assets/{layout-C_Qy9YRS.js → layout-CGI1vMRN.js} +1 -1
- package/src/client/dist/spa/assets/{liquid-2Xnhq08h.js → liquid-DhD-glYJ.js} +1 -1
- package/src/client/dist/spa/assets/{lspLanguageFeatures-Cd0LZuqI.js → lspLanguageFeatures-DsxRGZID.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-BcFCQf8z.js → mdx-BQKAfE4m.js} +1 -1
- package/src/client/dist/spa/assets/{monaco.contribution-BFELAVjU.js → monaco.contribution-BxeTJn7e.js} +2 -2
- package/src/client/dist/spa/assets/{network-auth-HNTN0Fqx.js → network-auth-ClL9UkXB.js} +1 -1
- package/src/client/dist/spa/assets/{project-color-CnA7qsFT.js → project-color-hgkIRO39.js} +1 -1
- package/src/client/dist/spa/assets/{python-mxDs2yew.js → python-7eduqhxd.js} +1 -1
- package/src/client/dist/spa/assets/{razor-Dwp1v9Mw.js → razor-DsfMRZhn.js} +1 -1
- package/src/client/dist/spa/assets/{render-chat-markdown-C66I22rl.js → render-chat-markdown-jyoLsnBV.js} +18 -18
- package/src/client/dist/spa/assets/{tsMode-DmV0n4aC.js → tsMode-BBuJqcnA.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-CPU1RgLJ.js → typescript-C1QMF4IF.js} +1 -1
- package/src/client/dist/spa/assets/{use-onboarding-ockQMKKz.js → use-onboarding-CR4gkqI9.js} +1 -1
- package/src/client/dist/spa/assets/{use-quasar-CCWojS5V.js → use-quasar-B65SkvJp.js} +1 -1
- package/src/client/dist/spa/assets/{workers-BUpbuYJJ.js → workers-kAnE9Ph9.js} +1 -1
- package/src/client/dist/spa/assets/{xml-BCZKO1fH.js → xml-DlrR7f2a.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-DT8wC8CS.js → yaml-CNExkyN_.js} +1 -1
- package/src/client/dist/spa/index.html +1 -1
- package/src/mcp-server/README.md +46 -1
- package/src/mcp-server/kobo-tasks-handlers.ts +37 -0
- package/src/mcp-server/kobo-tasks-server.ts +496 -328
- package/src/client/dist/spa/assets/CreatePage-0WIKLSHx.js +0 -2
- package/src/client/dist/spa/assets/CreatePage-Cyo8mr_a.css +0 -1
- package/src/client/dist/spa/assets/DiffViewer-JFUB2IjX.js +0 -8
- package/src/client/dist/spa/assets/build-path-tree-B2W0g5KD.js +0 -1
- package/src/client/dist/spa/assets/i18n-D5hQoijX.js +0 -1
|
@@ -4,15 +4,11 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
5
|
import { getDb } from '../server/db/index.js';
|
|
6
6
|
import { runMigrations } from '../server/db/migrations.js';
|
|
7
|
-
import { createTaskHandler, cronListHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getTicketSourcesHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, logThoughtHandler, markAutoLoopReadyHandler, markTaskDoneHandler, readDocumentHandler, setWorkspaceAgentDescriptionHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
|
|
7
|
+
import { createTaskHandler, cronListHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getTicketSourcesHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, listWorkspacesHandler, logThoughtHandler, markAutoLoopReadyHandler, markTaskDoneHandler, readDocumentHandler, setWorkspaceAgentDescriptionHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
|
|
8
8
|
const workspaceId = process.env.KOBO_WORKSPACE_ID;
|
|
9
9
|
const dbPath = process.env.KOBO_DB_PATH;
|
|
10
10
|
const settingsPath = process.env.KOBO_SETTINGS_PATH;
|
|
11
11
|
const backendUrl = process.env.KOBO_BACKEND_URL ?? 'http://localhost:3000';
|
|
12
|
-
if (!workspaceId) {
|
|
13
|
-
console.error('[kobo-tasks-server] KOBO_WORKSPACE_ID env var is required');
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
12
|
if (!dbPath) {
|
|
17
13
|
console.error('[kobo-tasks-server] KOBO_DB_PATH env var is required');
|
|
18
14
|
process.exit(1);
|
|
@@ -111,344 +107,433 @@ async function backendRequest(method, pathname, body) {
|
|
|
111
107
|
return text ? JSON.parse(text) : null;
|
|
112
108
|
}
|
|
113
109
|
const server = new Server({ name: 'kobo-tasks', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
110
|
+
const WORKSPACE_SCOPED_TOOLS = [
|
|
111
|
+
{
|
|
112
|
+
name: 'list_tasks',
|
|
113
|
+
description: 'CALL FIRST on any non-trivial turn to know what the user wants done and what is already completed. Returns every task and acceptance criterion for the current workspace with its id and status. Re-call periodically (before marking something done, or after the user asks for a status) to stay in sync with user-added or external updates.',
|
|
114
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
115
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'mark_task_done',
|
|
119
|
+
description: 'CALL AS SOON AS a task or acceptance criterion is finished AND verified (tests pass, feature works, diff committed). Do not wait for the end of the turn — the user watches progress live and marking each item as it completes is the primary signal Kōbō uses to track you.',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
task_id: { type: 'string', description: 'Task id from list_tasks.' },
|
|
124
|
+
},
|
|
125
|
+
required: ['task_id'],
|
|
121
126
|
},
|
|
122
|
-
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'mark_auto_loop_ready',
|
|
131
|
+
description: 'CALL ONLY at the end of a `/kobo-prep-autoloop` grooming session, once all tasks look atomic and implementable in one session. Flips a flag on the workspace that unlocks the auto-loop toggle in the UI. Do NOT call during normal sessions.',
|
|
132
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
133
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'create_task',
|
|
137
|
+
description: 'CALL WHEN you discover follow-up work that was not in the original list and needs to stick around (e.g. "refactor this helper later", "add a test for edge case"). Appends at the end of the list. Do not use it for ephemeral internal notes — prefer log_thought for those.',
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
title: { type: 'string', description: 'Short, imperative title (e.g. "Add retry to fetchUser").' },
|
|
142
|
+
is_acceptance_criterion: {
|
|
143
|
+
type: 'boolean',
|
|
144
|
+
description: 'Mark as acceptance criterion rather than a task (default: false).',
|
|
129
145
|
},
|
|
130
|
-
required: ['task_id'],
|
|
131
146
|
},
|
|
132
|
-
|
|
147
|
+
required: ['title'],
|
|
133
148
|
},
|
|
134
|
-
{
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'update_task',
|
|
153
|
+
description: 'CALL WHEN you need to refine a task — rewording for clarity, flipping status to `in_progress` as you start it, or promoting a task to acceptance criterion. At least one mutable field is required.',
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: {
|
|
157
|
+
task_id: { type: 'string', description: 'Task id from list_tasks.' },
|
|
158
|
+
title: { type: 'string', description: 'New title (optional).' },
|
|
159
|
+
status: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
enum: ['pending', 'in_progress', 'done'],
|
|
162
|
+
description: 'New status (optional).',
|
|
163
|
+
},
|
|
164
|
+
is_acceptance_criterion: {
|
|
165
|
+
type: 'boolean',
|
|
166
|
+
description: 'Toggle acceptance criterion flag (optional).',
|
|
151
167
|
},
|
|
152
|
-
required: ['title'],
|
|
153
168
|
},
|
|
154
|
-
|
|
169
|
+
required: ['task_id'],
|
|
155
170
|
},
|
|
156
|
-
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
type: 'string',
|
|
166
|
-
enum: ['pending', 'in_progress', 'done'],
|
|
167
|
-
description: 'New status (optional).',
|
|
168
|
-
},
|
|
169
|
-
is_acceptance_criterion: {
|
|
170
|
-
type: 'boolean',
|
|
171
|
-
description: 'Toggle acceptance criterion flag (optional).',
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
required: ['task_id'],
|
|
171
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'delete_task',
|
|
175
|
+
description: 'CALL ONLY when a task was created in error or became truly irrelevant (scope change validated by user). Prefer marking done or in_progress over deleting.',
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
properties: {
|
|
179
|
+
task_id: { type: 'string', description: 'Task id from list_tasks.' },
|
|
175
180
|
},
|
|
176
|
-
|
|
181
|
+
required: ['task_id'],
|
|
177
182
|
},
|
|
178
|
-
{
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'get_workspace_info',
|
|
187
|
+
description: 'CALL EARLY in a session to confirm project path, working/source branch, worktree path, model, and notion link. Cheap read — useful when the user refers to "this workspace" or when you need the worktree path to locate files.',
|
|
188
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
189
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'set_workspace_agent_description',
|
|
193
|
+
description: "Set or clear the workspace's agent-side description (≤ 200 chars). Pass an empty string to clear. The user sees this string under the workspace title in the sidebar (it takes precedence over the user-controlled `description` field). Plain text only. The current value is available via get_workspace_info as agentDescription. NOTE: there is a separate user-controlled `description` field — do NOT try to write it; it has no MCP tool.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
description: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
description: 'Plain text, max 200 characters. Empty string clears the description.',
|
|
185
200
|
},
|
|
186
|
-
required: ['task_id'],
|
|
187
201
|
},
|
|
188
|
-
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
name: 'get_workspace_info',
|
|
192
|
-
description: 'CALL EARLY in a session to confirm project path, working/source branch, worktree path, model, and notion link. Cheap read — useful when the user refers to "this workspace" or when you need the worktree path to locate files.',
|
|
193
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
194
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
202
|
+
required: ['description'],
|
|
195
203
|
},
|
|
196
|
-
{
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'set_workspace_name',
|
|
208
|
+
description: 'Rename THIS workspace — sets the `name` shown in the sidebar and window title. Call this ONLY when the user explicitly asks you to rename the workspace — never rename it on your own initiative. Whitespace is collapsed and trimmed; the name cannot be empty and is capped at the configured max length. Distinct from `agent_description` (the one-line status summary) and from the user-controlled `description`. The current value is in get_workspace_info as `name`.',
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
name: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
description: 'New workspace name. Non-empty after trimming; control characters are stripped.',
|
|
206
215
|
},
|
|
207
|
-
required: ['description'],
|
|
208
216
|
},
|
|
209
|
-
|
|
217
|
+
required: ['name'],
|
|
210
218
|
},
|
|
211
|
-
{
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'cron_create',
|
|
223
|
+
description: 'Schedule a recurring trigger on THIS workspace. At each fire, Kōbō waits for the workspace to be idle (no active session) and then resumes the same conversation by injecting `prompt` as the next user message — same UX as `schedule_wakeup` but recurring. Skip-if-active: if a session is already running when the timer fires, that occurrence is skipped, the next occurrence is computed, and the cron continues. The cron persists across server restarts (skip-missed semantics on boot — no catchup spam). Delete with `cron_delete(id)`. Multiple crons per workspace are allowed.',
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: 'object',
|
|
226
|
+
properties: {
|
|
227
|
+
expression: {
|
|
228
|
+
type: 'string',
|
|
229
|
+
description: 'Standard 5-field cron expression (`min hour dom month dow`) or one of the helpers `@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly`. Example: `*/30 * * * *` = every 30 minutes; `0 9 * * 1` = every Monday at 9am. Validated at create time.',
|
|
230
|
+
},
|
|
231
|
+
prompt: {
|
|
232
|
+
type: 'string',
|
|
233
|
+
description: 'The prompt to inject as the next user message at each fire.',
|
|
234
|
+
},
|
|
235
|
+
label: {
|
|
236
|
+
type: 'string',
|
|
237
|
+
description: 'Optional human-readable label for the cron (shown in the UI).',
|
|
238
|
+
},
|
|
239
|
+
mode: {
|
|
240
|
+
type: 'string',
|
|
241
|
+
enum: ['resume', 'fresh'],
|
|
242
|
+
description: "How each fire is handled. 'resume' (default) pins the cron to the session you're calling from, so every fire continues THAT conversation by injecting `prompt` as the next user message — use this when the cron should follow up on ongoing work. 'fresh' starts a brand-new session at every fire with a clean context — use this for periodic checks (e.g. CI watch, daily standup) that don't need conversation continuity.",
|
|
243
|
+
},
|
|
244
|
+
oneShot: {
|
|
245
|
+
type: 'boolean',
|
|
246
|
+
description: "When true, the cron cancels itself after the first real fire (default false = recurring). Use this to schedule a single trigger at a specific cron-expressible time (e.g. `0 14 7 6 *` = next 7 June at 14:00) without it repeating yearly. Skip-active fires don't consume the one-shot — the cron retries at the next occurrence until it actually runs once.",
|
|
221
247
|
},
|
|
222
|
-
required: ['name'],
|
|
223
248
|
},
|
|
224
|
-
|
|
249
|
+
required: ['expression', 'prompt'],
|
|
225
250
|
},
|
|
226
|
-
{
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
},
|
|
236
|
-
prompt: {
|
|
237
|
-
type: 'string',
|
|
238
|
-
description: 'The prompt to inject as the next user message at each fire.',
|
|
239
|
-
},
|
|
240
|
-
label: {
|
|
241
|
-
type: 'string',
|
|
242
|
-
description: 'Optional human-readable label for the cron (shown in the UI).',
|
|
243
|
-
},
|
|
244
|
-
mode: {
|
|
245
|
-
type: 'string',
|
|
246
|
-
enum: ['resume', 'fresh'],
|
|
247
|
-
description: "How each fire is handled. 'resume' (default) pins the cron to the session you're calling from, so every fire continues THAT conversation by injecting `prompt` as the next user message — use this when the cron should follow up on ongoing work. 'fresh' starts a brand-new session at every fire with a clean context — use this for periodic checks (e.g. CI watch, daily standup) that don't need conversation continuity.",
|
|
248
|
-
},
|
|
249
|
-
oneShot: {
|
|
250
|
-
type: 'boolean',
|
|
251
|
-
description: "When true, the cron cancels itself after the first real fire (default false = recurring). Use this to schedule a single trigger at a specific cron-expressible time (e.g. `0 14 7 6 *` = next 7 June at 14:00) without it repeating yearly. Skip-active fires don't consume the one-shot — the cron retries at the next occurrence until it actually runs once.",
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
required: ['expression', 'prompt'],
|
|
251
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'cron_delete',
|
|
255
|
+
description: "Cancel a previously-armed cron by id. Idempotent — returns ok=true even if the id is unknown. Only the workspace's own crons can be cancelled (cron_list to see them).",
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: 'object',
|
|
258
|
+
properties: {
|
|
259
|
+
id: { type: 'string', description: 'The cron id returned by cron_create.' },
|
|
255
260
|
},
|
|
256
|
-
|
|
261
|
+
required: ['id'],
|
|
257
262
|
},
|
|
258
|
-
{
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'cron_list',
|
|
267
|
+
description: 'List all crons currently armed on THIS workspace, including their next and last fire times.',
|
|
268
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
269
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'get_git_info',
|
|
273
|
+
description: 'CALL BEFORE creating a PR, committing in batches, or reporting progress to the user. Returns commit count ahead of source, files changed, insertions/deletions, and existing PR URL if any.',
|
|
274
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
275
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'set_workspace_status',
|
|
279
|
+
description: 'CALL WHEN you believe the mission is done (`completed`), blocked beyond recovery (`error`), or explicitly idle awaiting user input (`idle`). Transitions are validated by the backend — invalid ones are rejected.',
|
|
280
|
+
inputSchema: {
|
|
281
|
+
type: 'object',
|
|
282
|
+
properties: {
|
|
283
|
+
status: {
|
|
284
|
+
type: 'string',
|
|
285
|
+
enum: ['idle', 'completed', 'error'],
|
|
286
|
+
description: 'Target status.',
|
|
265
287
|
},
|
|
266
|
-
required: ['id'],
|
|
267
288
|
},
|
|
268
|
-
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
name: 'cron_list',
|
|
272
|
-
description: 'List all crons currently armed on THIS workspace, including their next and last fire times.',
|
|
273
|
-
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
274
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
289
|
+
required: ['status'],
|
|
275
290
|
},
|
|
276
|
-
{
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
291
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: 'get_ticket',
|
|
295
|
+
description: 'CALL when the user references "the ticket", "the issue", "the Notion page", or when you need the source-of-truth text for the mission. Works for any source — a Notion ticket or a Sentry issue. Returns `{ sources: [{ type, url, content }] }`, one entry per imported ticket (type is "notion" or "sentry"). Usually one source; empty when none was imported.',
|
|
296
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
297
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'get_dev_server_status',
|
|
301
|
+
description: 'CALL BEFORE asking the user whether the app is running, or when your change is dev-server-sensitive. Returns running/stopped/starting/error + URL, port, container names.',
|
|
302
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
303
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: 'start_dev_server',
|
|
307
|
+
description: 'CALL WHEN the user asks you to test the running app and the dev server is stopped.',
|
|
308
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
309
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: 'stop_dev_server',
|
|
313
|
+
description: 'CALL WHEN the user explicitly asks to stop the dev server, or before destructive operations that require a clean boot.',
|
|
314
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
315
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: 'get_dev_server_logs',
|
|
319
|
+
description: 'CALL WHEN debugging a runtime issue the user describes as happening in the running app. Returns the last N lines of logs (default 200). Cheaper than asking the user to paste them.',
|
|
320
|
+
inputSchema: {
|
|
321
|
+
type: 'object',
|
|
322
|
+
properties: {
|
|
323
|
+
tail: { type: 'number', description: 'Number of lines from the end (default: 200).' },
|
|
324
|
+
},
|
|
325
|
+
required: [],
|
|
281
326
|
},
|
|
282
|
-
{
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
327
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: 'list_workspace_images',
|
|
331
|
+
description: 'CALL WHEN the user mentions "the screenshot", "the attached image", or when you need to reference a previously-uploaded image. Returns uid, originalName, relativePath, createdAt for every image in .ai/images/.',
|
|
332
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
333
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: 'get_settings',
|
|
337
|
+
description: 'CALL WHEN you need to confirm configured models, PR prompt templates, git conventions, or dev-server commands before acting on them. Pass project_path to merge global + project-specific entries.',
|
|
338
|
+
inputSchema: {
|
|
339
|
+
type: 'object',
|
|
340
|
+
properties: {
|
|
341
|
+
project_path: {
|
|
342
|
+
type: 'string',
|
|
343
|
+
description: 'Project path to resolve a specific project entry (optional).',
|
|
293
344
|
},
|
|
294
|
-
required: ['status'],
|
|
295
345
|
},
|
|
296
|
-
|
|
346
|
+
required: [],
|
|
297
347
|
},
|
|
298
|
-
{
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
{
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
{
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
{
|
|
317
|
-
name: 'stop_dev_server',
|
|
318
|
-
description: 'CALL WHEN the user explicitly asks to stop the dev server, or before destructive operations that require a clean boot.',
|
|
319
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
320
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
321
|
-
},
|
|
322
|
-
{
|
|
323
|
-
name: 'get_dev_server_logs',
|
|
324
|
-
description: 'CALL WHEN debugging a runtime issue the user describes as happening in the running app. Returns the last N lines of logs (default 200). Cheaper than asking the user to paste them.',
|
|
325
|
-
inputSchema: {
|
|
326
|
-
type: 'object',
|
|
327
|
-
properties: {
|
|
328
|
-
tail: { type: 'number', description: 'Number of lines from the end (default: 200).' },
|
|
348
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
349
|
+
},
|
|
350
|
+
// ── Knowledge / context tools ─────────────────────────────────────────────
|
|
351
|
+
{
|
|
352
|
+
name: 'list_documents',
|
|
353
|
+
description: 'CALL EARLY on a new session to discover plans, specs, and thoughts previously written for this workspace. Recursively lists every .md under docs/plans/, docs/superpowers/, and .ai/thoughts/. Before writing a new plan, check if one already exists.',
|
|
354
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
355
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: 'read_document',
|
|
359
|
+
description: 'CALL AFTER list_documents when a file title looks relevant to the current task. Returns the full markdown content. Scoped to docs/plans/, docs/superpowers/, .ai/thoughts/ — reject anything else.',
|
|
360
|
+
inputSchema: {
|
|
361
|
+
type: 'object',
|
|
362
|
+
properties: {
|
|
363
|
+
path: {
|
|
364
|
+
type: 'string',
|
|
365
|
+
description: 'Worktree-relative path from list_documents (e.g. "docs/superpowers/plans/2026-04-17-foo.md").',
|
|
329
366
|
},
|
|
330
|
-
required: [],
|
|
331
367
|
},
|
|
332
|
-
|
|
368
|
+
required: ['path'],
|
|
333
369
|
},
|
|
334
|
-
{
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
type: 'string',
|
|
348
|
-
description: 'Project path to resolve a specific project entry (optional).',
|
|
349
|
-
},
|
|
370
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: 'log_thought',
|
|
374
|
+
description: 'CALL WHEN you make a decision worth remembering — architecture choice, trade-off taken, dead-end avoided, pattern discovered. Appends a dated markdown file to .ai/thoughts/logs/. Keep entries short and focused; one decision per call. Use create_task for actionable follow-ups instead.',
|
|
375
|
+
inputSchema: {
|
|
376
|
+
type: 'object',
|
|
377
|
+
properties: {
|
|
378
|
+
title: { type: 'string', description: 'Short, descriptive title (becomes the filename slug and the # H1).' },
|
|
379
|
+
content: { type: 'string', description: 'Markdown body explaining the decision and its reasoning.' },
|
|
380
|
+
tag: {
|
|
381
|
+
type: 'string',
|
|
382
|
+
description: 'Optional short tag appended to filename (e.g. "arch", "bug", "perf").',
|
|
350
383
|
},
|
|
351
|
-
required: [],
|
|
352
384
|
},
|
|
353
|
-
|
|
354
|
-
},
|
|
355
|
-
// ── Knowledge / context tools ─────────────────────────────────────────────
|
|
356
|
-
{
|
|
357
|
-
name: 'list_documents',
|
|
358
|
-
description: 'CALL EARLY on a new session to discover plans, specs, and thoughts previously written for this workspace. Recursively lists every .md under docs/plans/, docs/superpowers/, and .ai/thoughts/. Before writing a new plan, check if one already exists.',
|
|
359
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
360
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
385
|
+
required: ['title', 'content'],
|
|
361
386
|
},
|
|
362
|
-
{
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
387
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: 'search_codebase',
|
|
391
|
+
description: 'CALL WHEN you need to recall prior chat history across workspaces — past decisions, prior user requests, an agent message you remember but can’t locate. Full-text search over user messages + agent outputs persisted in Kōbō. Use the local Grep tool for searching source code; this tool searches CONVERSATIONS.',
|
|
392
|
+
inputSchema: {
|
|
393
|
+
type: 'object',
|
|
394
|
+
properties: {
|
|
395
|
+
query: { type: 'string', description: 'Search phrase. Plain text; no regex.' },
|
|
396
|
+
include_archived: {
|
|
397
|
+
type: 'boolean',
|
|
398
|
+
description: 'Include archived workspaces in the search (default: false).',
|
|
372
399
|
},
|
|
373
|
-
|
|
400
|
+
scope: {
|
|
401
|
+
type: 'string',
|
|
402
|
+
enum: ['workspace', 'all'],
|
|
403
|
+
description: 'Restrict to this workspace only (default) or search across every workspace.',
|
|
404
|
+
},
|
|
405
|
+
limit: { type: 'number', description: 'Max results to return (default 30, max 100).' },
|
|
374
406
|
},
|
|
375
|
-
|
|
407
|
+
required: ['query'],
|
|
376
408
|
},
|
|
377
|
-
{
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
409
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'get_session_usage',
|
|
413
|
+
description: 'CALL when you need to self-regulate on long missions — returns token/cost totals for the workspace lifetime and for the currently running agent_session. Useful before spawning heavy subagents or deep reasoning on already-expensive sessions.',
|
|
414
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
415
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: 'schedule_wakeup',
|
|
419
|
+
description: 'WHEN: use this whenever you would otherwise WAIT for or POLL a long-running task (test suite, CI, build, deploy, or external state) — never block your turn with sleep/poll loops, and never end a turn merely "waiting" on a background job, because the session goes idle and will not resume on its own. CALL to schedule a follow-up turn on THIS workspace after a delay. End the current turn normally; once it finishes and the workspace is idle, Kōbō waits `delaySeconds`, then resumes the same conversation by injecting `prompt` as the next user message. The wakeup is scoped to the current workspace and resumes its latest session — you cannot target another workspace or another session. If a turn is still active when the timer fires, the wakeup is skipped (status: `session-active`). Replaces any previously pending wakeup on this workspace. Delay is clamped to [60, 21600] seconds (1min to 6h). Prefer this over the built-in `ScheduleWakeup` tool — it is the SDK-supported entry point.',
|
|
420
|
+
inputSchema: {
|
|
421
|
+
type: 'object',
|
|
422
|
+
properties: {
|
|
423
|
+
delaySeconds: {
|
|
424
|
+
type: 'number',
|
|
425
|
+
description: 'Seconds from now until the wakeup fires. Clamped to [60, 21600] (1min to 6h).',
|
|
426
|
+
},
|
|
427
|
+
prompt: {
|
|
428
|
+
type: 'string',
|
|
429
|
+
description: 'Prompt sent to the agent when the wakeup fires.',
|
|
430
|
+
},
|
|
431
|
+
reason: {
|
|
432
|
+
type: 'string',
|
|
433
|
+
description: 'Short label shown to the user explaining the wakeup (optional).',
|
|
389
434
|
},
|
|
390
|
-
required: ['title', 'content'],
|
|
391
435
|
},
|
|
392
|
-
|
|
436
|
+
required: ['delaySeconds', 'prompt'],
|
|
393
437
|
},
|
|
394
|
-
{
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
438
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
name: 'cancel_wakeup',
|
|
442
|
+
description: 'CALL to cancel any pending wakeup on this workspace (e.g. the condition you were waiting on resolved early, or you decided not to continue). Idempotent — safe to call when nothing is pending.',
|
|
443
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
444
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
445
|
+
},
|
|
446
|
+
];
|
|
447
|
+
const GLOBAL_TOOLS = [
|
|
448
|
+
{
|
|
449
|
+
name: 'list_workspaces',
|
|
450
|
+
description: 'List Kōbō workspaces with their id, title, status, and creation date. Works even when the Kōbō backend server is not running (reads the database directly). Use this to discover existing workspaces before creating one or targeting one with archive_workspace/stop_workspace.',
|
|
451
|
+
inputSchema: {
|
|
452
|
+
type: 'object',
|
|
453
|
+
properties: {
|
|
454
|
+
include_archived: {
|
|
455
|
+
type: 'boolean',
|
|
456
|
+
description: 'Include archived workspaces (default false).',
|
|
411
457
|
},
|
|
412
|
-
required: ['query'],
|
|
413
458
|
},
|
|
414
|
-
|
|
415
|
-
},
|
|
416
|
-
{
|
|
417
|
-
name: 'get_session_usage',
|
|
418
|
-
description: 'CALL when you need to self-regulate on long missions — returns token/cost totals for the workspace lifetime and for the currently running agent_session. Useful before spawning heavy subagents or deep reasoning on already-expensive sessions.',
|
|
419
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
420
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
459
|
+
required: [],
|
|
421
460
|
},
|
|
422
|
-
{
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
461
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
name: 'create_workspace',
|
|
465
|
+
description: 'Create a new Kōbō workspace (git worktree + agent session), like the "Créer" button on the Create page. Requires the Kōbō backend server to be running and reachable at KOBO_BACKEND_URL. name/project_path/source_branch/working_branch are mandatory — unlike the UI, this API does not auto-derive a branch name from the workspace name.',
|
|
466
|
+
inputSchema: {
|
|
467
|
+
type: 'object',
|
|
468
|
+
properties: {
|
|
469
|
+
name: { type: 'string', description: 'Workspace display name.' },
|
|
470
|
+
project_path: { type: 'string', description: 'Absolute path to the project on disk.' },
|
|
471
|
+
source_branch: { type: 'string', description: 'Branch to base the new worktree on (e.g. "develop").' },
|
|
472
|
+
working_branch: {
|
|
473
|
+
type: 'string',
|
|
474
|
+
description: 'New branch name for the worktree (e.g. "feature/my-thing").',
|
|
475
|
+
},
|
|
476
|
+
model: { type: 'string', description: 'Model id override (optional, defaults to project/global setting).' },
|
|
477
|
+
reasoning_effort: { type: 'string', description: 'Reasoning effort override (optional).' },
|
|
478
|
+
engine: { type: 'string', description: 'Agent engine id, e.g. "claude-code" or "codex" (optional).' },
|
|
479
|
+
description: { type: 'string', description: 'Task description / initial brainstorming prompt (optional).' },
|
|
480
|
+
tasks: { type: 'array', items: { type: 'string' }, description: 'Initial manual task titles (optional).' },
|
|
481
|
+
acceptance_criteria: {
|
|
482
|
+
type: 'array',
|
|
483
|
+
items: { type: 'string' },
|
|
484
|
+
description: 'Initial manual acceptance criteria (optional).',
|
|
485
|
+
},
|
|
486
|
+
agent_permission_mode: {
|
|
487
|
+
type: 'string',
|
|
488
|
+
enum: ['plan', 'bypass', 'strict', 'interactive'],
|
|
489
|
+
description: 'Permission mode for the new session (optional).',
|
|
440
490
|
},
|
|
441
|
-
|
|
491
|
+
auto_loop: { type: 'boolean', description: 'Start the workspace in auto-loop mode (optional).' },
|
|
492
|
+
auto_loop_session_mode: {
|
|
493
|
+
type: 'string',
|
|
494
|
+
enum: ['per_task', 'continuous'],
|
|
495
|
+
description: "Auto-loop session mode when auto_loop is true (optional, default 'per_task').",
|
|
496
|
+
},
|
|
497
|
+
skip_setup_script: { type: 'boolean', description: "Skip the project's setup script (optional)." },
|
|
442
498
|
},
|
|
443
|
-
|
|
499
|
+
required: ['name', 'project_path', 'source_branch', 'working_branch'],
|
|
444
500
|
},
|
|
445
|
-
{
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
501
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
name: 'archive_workspace',
|
|
505
|
+
description: 'Archive a workspace by id, like the "Archiver" action in the workspace context menu. Requires the Kōbō backend server to be running.',
|
|
506
|
+
inputSchema: {
|
|
507
|
+
type: 'object',
|
|
508
|
+
properties: {
|
|
509
|
+
workspace_id: { type: 'string', description: 'Workspace id, from list_workspaces.' },
|
|
510
|
+
},
|
|
511
|
+
required: ['workspace_id'],
|
|
450
512
|
},
|
|
451
|
-
|
|
513
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: 'stop_workspace',
|
|
517
|
+
description: 'Force-stop the currently running agent session on a workspace, like the red "Arrêter" button in the chat header. Requires the Kōbō backend server to be running. Safe to call when nothing is running.',
|
|
518
|
+
inputSchema: {
|
|
519
|
+
type: 'object',
|
|
520
|
+
properties: {
|
|
521
|
+
workspace_id: { type: 'string', description: 'Workspace id, from list_workspaces.' },
|
|
522
|
+
},
|
|
523
|
+
required: ['workspace_id'],
|
|
524
|
+
},
|
|
525
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
526
|
+
},
|
|
527
|
+
];
|
|
528
|
+
/**
|
|
529
|
+
* Tool names callable without KOBO_WORKSPACE_ID set. Everything else requires
|
|
530
|
+
* a workspace-bound session — enforced once, up front, in the
|
|
531
|
+
* CallToolRequestSchema handler below rather than repeating a guard in every
|
|
532
|
+
* one of the ~35 workspace-scoped branches.
|
|
533
|
+
*/
|
|
534
|
+
const GLOBAL_TOOL_NAMES = new Set(GLOBAL_TOOLS.map((t) => t.name));
|
|
535
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
536
|
+
tools: workspaceId ? [...WORKSPACE_SCOPED_TOOLS, ...GLOBAL_TOOLS] : GLOBAL_TOOLS,
|
|
452
537
|
}));
|
|
453
538
|
/** Wrap a successful result as an MCP tool response with JSON text content. */
|
|
454
539
|
function ok(data) {
|
|
@@ -458,9 +543,26 @@ function ok(data) {
|
|
|
458
543
|
function fail(message) {
|
|
459
544
|
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
460
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Turn a backendRequest() rejection into a user-facing message. backendRequest
|
|
548
|
+
* always throws `Backend ${method} ${pathname} returned ${status}: ...` for a
|
|
549
|
+
* non-2xx HTTP response (already a clear message — pass it through). Any
|
|
550
|
+
* other error (fetch itself throwing, e.g. ECONNREFUSED) means the backend
|
|
551
|
+
* process isn't reachable at all, which gets a friendlier, actionable message.
|
|
552
|
+
*/
|
|
553
|
+
function backendErrorMessage(err) {
|
|
554
|
+
if (err instanceof Error && err.message.startsWith('Backend ')) {
|
|
555
|
+
return err.message;
|
|
556
|
+
}
|
|
557
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
558
|
+
return `Kōbō backend unreachable at ${backendUrl} — is the app running? (${detail})`;
|
|
559
|
+
}
|
|
461
560
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
462
561
|
const { name, arguments: args } = request.params;
|
|
463
562
|
const a = (args ?? {});
|
|
563
|
+
if (!workspaceId && !GLOBAL_TOOL_NAMES.has(name)) {
|
|
564
|
+
return fail(`Tool '${name}' requires a workspace-bound MCP session (KOBO_WORKSPACE_ID not set).`);
|
|
565
|
+
}
|
|
464
566
|
try {
|
|
465
567
|
if (name === 'list_tasks') {
|
|
466
568
|
return ok(listTasksHandler(db, workspaceId));
|
|
@@ -701,6 +803,65 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
701
803
|
const results = scope === 'all' ? raw : raw.filter((r) => r.workspaceId === workspaceId);
|
|
702
804
|
return ok({ query, scope, total: results.length, results });
|
|
703
805
|
}
|
|
806
|
+
if (name === 'list_workspaces') {
|
|
807
|
+
const includeArchived = a.include_archived === true;
|
|
808
|
+
return ok(listWorkspacesHandler(db, { includeArchived }));
|
|
809
|
+
}
|
|
810
|
+
if (name === 'create_workspace') {
|
|
811
|
+
const workspaceName = a.name;
|
|
812
|
+
const projectPath = a.project_path;
|
|
813
|
+
const sourceBranch = a.source_branch;
|
|
814
|
+
const workingBranch = a.working_branch;
|
|
815
|
+
if (!workspaceName || !projectPath || !sourceBranch || !workingBranch) {
|
|
816
|
+
return fail('name, project_path, source_branch, and working_branch parameters are required');
|
|
817
|
+
}
|
|
818
|
+
try {
|
|
819
|
+
const created = await backendRequest('POST', '/api/workspaces', {
|
|
820
|
+
name: workspaceName,
|
|
821
|
+
projectPath,
|
|
822
|
+
sourceBranch,
|
|
823
|
+
workingBranch,
|
|
824
|
+
model: a.model,
|
|
825
|
+
reasoningEffort: a.reasoning_effort,
|
|
826
|
+
engine: a.engine,
|
|
827
|
+
description: a.description,
|
|
828
|
+
tasks: a.tasks,
|
|
829
|
+
acceptanceCriteria: a.acceptance_criteria,
|
|
830
|
+
agentPermissionMode: a.agent_permission_mode,
|
|
831
|
+
autoLoop: a.auto_loop,
|
|
832
|
+
autoLoopSessionMode: a.auto_loop_session_mode,
|
|
833
|
+
skipSetupScript: a.skip_setup_script,
|
|
834
|
+
});
|
|
835
|
+
return ok(created);
|
|
836
|
+
}
|
|
837
|
+
catch (err) {
|
|
838
|
+
return fail(backendErrorMessage(err));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (name === 'archive_workspace') {
|
|
842
|
+
const targetId = a.workspace_id;
|
|
843
|
+
if (!targetId)
|
|
844
|
+
return fail('workspace_id parameter is required');
|
|
845
|
+
try {
|
|
846
|
+
const result = await backendRequest('POST', `/api/workspaces/${targetId}/archive`);
|
|
847
|
+
return ok(result);
|
|
848
|
+
}
|
|
849
|
+
catch (err) {
|
|
850
|
+
return fail(backendErrorMessage(err));
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (name === 'stop_workspace') {
|
|
854
|
+
const targetId = a.workspace_id;
|
|
855
|
+
if (!targetId)
|
|
856
|
+
return fail('workspace_id parameter is required');
|
|
857
|
+
try {
|
|
858
|
+
const result = await backendRequest('POST', `/api/workspaces/${targetId}/stop`);
|
|
859
|
+
return ok(result);
|
|
860
|
+
}
|
|
861
|
+
catch (err) {
|
|
862
|
+
return fail(backendErrorMessage(err));
|
|
863
|
+
}
|
|
864
|
+
}
|
|
704
865
|
return fail(`Unknown tool: ${name}`);
|
|
705
866
|
}
|
|
706
867
|
catch (err) {
|