@loicngr/kobo 2.1.26 → 2.1.28
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/AGENTS.md +6 -3
- package/CHANGELOG.md +11 -0
- package/CONFIGURATION.md +97 -49
- package/SECURITY.md +33 -6
- package/dist/mcp-server/kobo-tasks-server.js +36 -1021
- package/dist/mcp-server/managed-mcp-dispatcher.js +960 -0
- package/dist/mcp-server/workspace-dialogue-client.js +26 -1
- package/dist/server/index.js +52 -19
- package/dist/server/middleware/host-check-middleware.js +8 -5
- package/dist/server/middleware/network-auth-middleware.js +51 -29
- package/dist/server/middleware/security-headers-middleware.js +19 -0
- package/dist/server/middleware/traffic-limits-middleware.js +74 -0
- package/dist/server/routes/auth.js +73 -0
- package/dist/server/routes/internal-mcp.js +137 -0
- package/dist/server/routes/mcp-credentials.js +30 -0
- package/dist/server/routes/mcp.js +156 -76
- package/dist/server/routes/workspaces.js +19 -3
- package/dist/server/services/agent/orchestrator.js +27 -17
- package/dist/server/services/external-mcp-credentials-service.js +110 -0
- package/dist/server/services/forge/github/provider.js +15 -3
- package/dist/server/services/forge/types.js +8 -0
- package/dist/server/services/internal-mcp-capability-service.js +98 -0
- package/dist/server/services/mcp-connection-info-service.js +1 -1
- package/dist/server/services/memory-mcp-service.js +9 -7
- package/dist/server/services/memory-service.js +15 -0
- package/dist/server/services/network-access-service.js +43 -73
- package/dist/server/services/network-session-service.js +182 -0
- package/dist/server/services/settings-service.js +29 -7
- package/dist/server/services/terminal-websocket-service.js +5 -0
- package/dist/server/services/websocket-service.js +5 -0
- package/dist/server/utils/paths.js +2 -1
- package/dist/server/utils/startup-display.js +1 -2
- package/dist/shared/models.js +6 -0
- package/docker-compose.vps.yml +6 -0
- package/docs/deployment-vps.md +24 -12
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-D58VS9o2.js +2 -0
- package/src/client/dist/pwa/assets/{AutoLoopFinalReviewSettings-DOr41E2Y.js → AutoLoopFinalReviewSettings-CK3r_fVB.js} +1 -1
- package/src/client/dist/pwa/assets/{ChangelogPage-DOCgZMpn.js → ChangelogPage-CAAUEn5X.js} +1 -1
- package/src/client/dist/pwa/assets/ComparisonPanel-BCb7vB5E.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-DasH1Uk4.js +2 -0
- package/src/client/dist/pwa/assets/{DashboardPage-dBpJaQSb.js → DashboardPage-XE52EABP.js} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-CncKlXne.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-AqLx0MzB.js → DocumentsPanel-L4pbQfz1.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-BLWaFgh0.js → DrawerToggleButton-DiOJm5he.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-WJlzy2VJ.js +3 -0
- package/src/client/dist/pwa/assets/{GitPanel-CRaLJzjc.css → GitPanel-yHvU38dp.css} +1 -1
- package/src/client/dist/pwa/assets/{HealthPage-DeryVLyz.js → HealthPage-hJY5MurK.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-DgWElK7w.js +2 -0
- package/src/client/dist/pwa/assets/{MemoryPanel-B34DENtz.js → MemoryPanel-DtEvh_b7.js} +1 -1
- package/src/client/dist/pwa/assets/{MemoryProposalList-BpN_OU8J.js → MemoryProposalList-kgFCm9zz.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnDropdown-BPcK_rVs.js → QBtnDropdown-CWPIugs8.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-CpwQ8L_F.js → QExpansionItem-D-EGL2b3.js} +1 -1
- package/src/client/dist/pwa/assets/{QScrollArea-BkNSVl7s.js → QScrollArea-BMrqK-xd.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-DFWyDWCE.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-Dp8fitHV.js → QTooltip-Bk8dLUtU.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-Ck8LKDCa.js → SchedulePanel-CuxmxCKi.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-BAUZg1zq.js → SearchPage-DKnQPUqY.js} +1 -1
- package/src/client/dist/pwa/assets/{SettingsPage-Dcq1Jeoo.css → SettingsPage-B4rmd2WX.css} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-CO3vIpPd.js +9 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-CH8OHmtn.js → SplitWorkspacePage-WhQyX8fn.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-CBmZ0IzQ.js → TerminalPanel-BY0lPq6k.js} +10 -10
- package/src/client/dist/pwa/assets/{TerminalPanel-Dz9EE-Ph.css → TerminalPanel-BcbwuFVR.css} +1 -1
- package/src/client/dist/pwa/assets/{ToolCallItem-CLtGTV6w.js → ToolCallItem-DYubi-J0.js} +3 -3
- package/src/client/dist/pwa/assets/{TouchPan-9SH2dx6m.js → TouchPan-DDotyYLN.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-Dw4sGf9B.js → TourReplayButton-kAnEMcy2.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-wd_sObJa.js → WorkspaceAttentionLabels-DiYt3rP3.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-K-1ce50d.js +4 -0
- package/src/client/dist/pwa/assets/{browser-CC4W8r76.js → browser-DVYLH8vl.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-BBuyB-my.js → build-path-tree-DEuOdIIk.js} +1 -1
- package/src/client/dist/pwa/assets/custom-sounds-Ggw6V9iF.js +1 -0
- package/src/client/dist/pwa/assets/de-Bk3zQrin.js +17 -0
- package/src/client/dist/pwa/assets/{editor.api-BwfxvQ88.js → editor.api-BC38eXAS.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-B3I7w5bs.js → efforts-TGacUPcJ.js} +1 -1
- package/src/client/dist/pwa/assets/es-f3xuAWA9.js +17 -0
- package/src/client/dist/pwa/assets/{expand-template-Ci9UjnxZ.js → expand-template-DM8gq3Mo.js} +1 -1
- package/src/client/dist/pwa/assets/{format-CJIzi3Vf.js → format-OwxXBnDJ.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-DQ8cP7nO.js → formatters-YgaCZcwJ.js} +1 -1
- package/src/client/dist/pwa/assets/fr-B6c21SG3.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DbpVO-cL.js → freemarker2-l7R42fI2.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BHve0PpA.js → handlebars-DFmPrgjH.js} +1 -1
- package/src/client/dist/pwa/assets/{html-BhUkdFPz.js → html-CERIAlPM.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-WaE4cZK9.js +1 -0
- package/src/client/dist/pwa/assets/index-CpWBGKdM.js +20 -0
- package/src/client/dist/pwa/assets/it-DUstDpC2.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-BLAorPa3.js → javascript-BBYncplF.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-CJGF-h-f.js → liquid-C1HqYyYU.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-Cm0oNz_l.js → mdx-E74dsP22.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-nBg6wf05.js → monaco-contributions-DHvm-kp5.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-hr9s26Ud.js → monaco.contribution-DYdn9CwR.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-DTZUKbny.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-EcbxuH_8.js → notify-theme-FuTL7HuS.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-DLWiBfdb.js → permissionModes-D-Nexnoo.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-vDuTaTJn.js → project-color-_snXWYUp.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BEjbzFAQ.js → python-DNAAGBcX.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-BJPT7Wxx.js → razor-CB3g5p_T.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-DPqM46Ef.js → typescript-0kpz8bqP.js} +1 -1
- package/src/client/dist/pwa/assets/{use-search-index-status-BwwrB410.js → use-search-index-status-D38yFOD_.js} +1 -1
- package/src/client/dist/pwa/assets/{use-tours-B0KtCS9w.js → use-tours-BWkHp34X.js} +2 -2
- package/src/client/dist/pwa/assets/{use-worktree-restore-tzd2FESU.js → use-worktree-restore-DUjZr44N.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-CubzsZUU.js → workspace-templates-D-lPJQ4G.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-C6SVzw5_.js → xml-BbOuqdUe.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-OpCqnv4g.js → yaml-gasFQtfk.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +13 -19
- package/src/mcp-server/kobo-tasks-server.ts +42 -1146
- package/src/mcp-server/managed-mcp-dispatcher.ts +1071 -0
- package/src/mcp-server/workspace-dialogue-client.ts +26 -1
- package/src/client/dist/pwa/assets/ActivityFeed-DfYrVNEh.js +0 -2
- package/src/client/dist/pwa/assets/ComparisonPanel-b5nTBjK6.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-B7VPHEqm.js +0 -2
- package/src/client/dist/pwa/assets/DiffViewer-BLQz2Qyx.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-Ba7ltCsM.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-B2XvjdWi.js +0 -2
- package/src/client/dist/pwa/assets/QSelect-DrLpbvcr.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Cj6kSVT5.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-D4ODPOG-.js +0 -4
- package/src/client/dist/pwa/assets/custom-sounds-BbPNRzAs.js +0 -1
- package/src/client/dist/pwa/assets/de-D9VzPDGF.js +0 -17
- package/src/client/dist/pwa/assets/es-3R4MJuTt.js +0 -17
- package/src/client/dist/pwa/assets/fr-DXwDVnJ4.js +0 -17
- package/src/client/dist/pwa/assets/i18n-CGLQAADf.js +0 -1
- package/src/client/dist/pwa/assets/index-CNcXBpzz.js +0 -20
- package/src/client/dist/pwa/assets/it-T9AjnG42.js +0 -17
- package/src/client/dist/pwa/assets/network-auth-C850jGfI.js +0 -1
|
@@ -2,1039 +2,54 @@
|
|
|
2
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
-
import { getDb } from '../server/db/index.js';
|
|
6
5
|
import { MAX_MCP_ATTACHMENT_REQUEST_BYTES } from '../shared/mcp-attachments.js';
|
|
7
|
-
import {
|
|
8
|
-
import { INTERNAL_PROGRESS_BAR_TOOLS, isProgressBarTool, PROGRESS_BAR_TOOLS } from '../shared/progress-bar-tools.js';
|
|
9
|
-
import { isWorkspaceCreationTool, WORKSPACE_CREATION_TOOLS } from '../shared/workspace-creation-tools.js';
|
|
10
|
-
import { WORKSPACE_DIALOGUE_TOOLS } from '../shared/workspace-dialogue-tools.js';
|
|
11
|
-
import { isWorkspaceGroupMessageTool, WORKSPACE_GROUP_MESSAGE_TOOLS } from '../shared/workspace-group-message-tools.js';
|
|
12
|
-
import { isWorkspaceLifecycleTool, WORKSPACE_LIFECYCLE_TOOLS } from '../shared/workspace-lifecycle-tools.js';
|
|
13
|
-
import { createTaskHandler, cronListHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getTicketSourcesHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, listWorkspacesHandler, logThoughtHandler, markAutoLoopReadyHandler, markTaskDoneHandler, readDocumentHandler, readWorkspaceEventsCsvHandler, setWorkspaceAgentDescriptionHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
|
|
14
|
-
import { callMemoryTool, isMemoryOutputSuppressed } from './memory-client.js';
|
|
15
|
-
import { callProgressBarTool } from './progress-bar-client.js';
|
|
16
|
-
import { callWorkspaceDialogueTool } from './workspace-dialogue-client.js';
|
|
17
|
-
const finalReviewToken = process.env.KOBO_FINAL_REVIEW_TOKEN;
|
|
18
|
-
const finalReviewSessionId = process.env.KOBO_FINAL_REVIEW_SESSION_ID;
|
|
19
|
-
const handoffId = process.env.KOBO_HANDOFF_ID;
|
|
20
|
-
const handoffToken = process.env.KOBO_HANDOFF_TOKEN;
|
|
6
|
+
import { callWorkspaceDialogueTool, listWorkspaceDialogueTools } from './workspace-dialogue-client.js';
|
|
21
7
|
const workspaceId = process.env.KOBO_WORKSPACE_ID;
|
|
22
|
-
const
|
|
23
|
-
const settingsPath = process.env.KOBO_SETTINGS_PATH;
|
|
8
|
+
const capability = process.env.KOBO_MCP_SESSION_TOKEN;
|
|
24
9
|
const backendUrl = process.env.KOBO_BACKEND_URL ?? 'http://localhost:3000';
|
|
25
|
-
if (!dbPath) {
|
|
26
|
-
console.error('[kobo-tasks-server] KOBO_DB_PATH env var is required');
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
let db;
|
|
30
|
-
try {
|
|
31
|
-
// Use the shared `getDb` singleton so any service that calls `getDb()`
|
|
32
|
-
// internally (e.g. cron-service.arm) hits the SAME connection as this
|
|
33
|
-
// MCP server, against the SAME DB file. Without this bootstrap, the
|
|
34
|
-
// singleton would resolve via getDbPath() → getKoboHome() → KOBO_HOME
|
|
35
|
-
// env var, which the agent SDK does NOT pass to the MCP server (only
|
|
36
|
-
// KOBO_DB_PATH is passed). That would silently open a second connection
|
|
37
|
-
// against the user's prod DB at ~/.config/kobo/kobo.db, which may not
|
|
38
|
-
// even have the same schema as the dev DB the backend is using.
|
|
39
|
-
// On ouvre SANS migrer. Ce process est lancé une fois par workspace, sur le
|
|
40
|
-
// même fichier de base : `runMigrations` calcule l'ensemble des blocs
|
|
41
|
-
// appliqués AVANT de prendre le verrou, donc deux serveurs MCP démarrés
|
|
42
|
-
// ensemble peuvent rejouer le même bloc — et les blocs v2 à v19 ajoutent
|
|
43
|
-
// des colonnes sans garde d'existence. Un `duplicate column name` ici
|
|
44
|
-
// termine le process et prive l'agent de tous ses outils en pleine session.
|
|
45
|
-
// Le backend migre au démarrage, et il démarre nécessairement avant de
|
|
46
|
-
// pouvoir lancer ce serveur.
|
|
47
|
-
db = getDb(dbPath);
|
|
48
|
-
}
|
|
49
|
-
catch (err) {
|
|
50
|
-
console.error('[kobo-tasks-server] Failed to open database:', err);
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
/** Fire-and-forget POST to the backend so the UI reflects a task marked as done. */
|
|
54
|
-
async function notifyBackend(taskId) {
|
|
55
|
-
try {
|
|
56
|
-
const url = `${backendUrl}/api/workspaces/${workspaceId}/tasks/${taskId}/notify-done`;
|
|
57
|
-
const res = await fetch(url, { method: 'POST' });
|
|
58
|
-
if (!res.ok) {
|
|
59
|
-
console.error(`[kobo-tasks-server] notify-done HTTP ${res.status}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
catch (err) {
|
|
63
|
-
console.error('[kobo-tasks-server] notify-done failed:', err);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/** Fire-and-forget POST to the backend so the UI refreshes the task list after a mutation. */
|
|
67
|
-
async function notifyTasksUpdated() {
|
|
68
|
-
try {
|
|
69
|
-
const url = `${backendUrl}/api/workspaces/${workspaceId}/tasks/notify-updated`;
|
|
70
|
-
await fetch(url, { method: 'POST' });
|
|
71
|
-
}
|
|
72
|
-
catch (err) {
|
|
73
|
-
console.error('[kobo-tasks-server] notify-updated failed:', err);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Fire-and-forget POST that lands on `/auto-loop-ready`, which itself emits
|
|
78
|
-
* the `autoloop:ready-flipped` WS event so the frontend's toggle unlocks
|
|
79
|
-
* immediately after the grooming session completes. The handler already
|
|
80
|
-
* flipped the DB flag; this call is ONLY for the event emission + the
|
|
81
|
-
* (harmless) idempotent second write.
|
|
82
|
-
*/
|
|
83
|
-
async function notifyAutoLoopReady() {
|
|
84
|
-
try {
|
|
85
|
-
const url = `${backendUrl}/api/workspaces/${workspaceId}/auto-loop-ready`;
|
|
86
|
-
await fetch(url, { method: 'POST' });
|
|
87
|
-
}
|
|
88
|
-
catch (err) {
|
|
89
|
-
console.error('[kobo-tasks-server] notify-autoloop-ready failed:', err);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Fire-and-forget POST that lands on `/agent-description/notify-updated`,
|
|
94
|
-
* which emits the `workspace:agent-description-updated` WS event so the
|
|
95
|
-
* sidebar fallback display + the workspace header italic line refresh live
|
|
96
|
-
* across every connected client. The handler already wrote the column to DB;
|
|
97
|
-
* this call is ONLY for the event emission.
|
|
98
|
-
*/
|
|
99
|
-
async function notifyAgentDescriptionUpdated() {
|
|
100
|
-
try {
|
|
101
|
-
const url = `${backendUrl}/api/workspaces/${workspaceId}/agent-description/notify-updated`;
|
|
102
|
-
await fetch(url, { method: 'POST' });
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
console.error('[kobo-tasks-server] notify-agent-description-updated failed:', err);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
/** Generic HTTP request to the Kobo backend, returning parsed JSON or null. */
|
|
109
|
-
async function backendRequest(method, pathname, body) {
|
|
110
|
-
const url = `${backendUrl}${pathname}`;
|
|
111
|
-
const headers = process.env.KOBO_NETWORK_TOKEN
|
|
112
|
-
? { 'X-Kobo-Token': process.env.KOBO_NETWORK_TOKEN }
|
|
113
|
-
: {};
|
|
114
|
-
const init = { method, headers };
|
|
115
|
-
if (body !== undefined) {
|
|
116
|
-
headers['Content-Type'] = 'application/json';
|
|
117
|
-
init.body = JSON.stringify(body);
|
|
118
|
-
}
|
|
119
|
-
const res = await fetch(url, init);
|
|
120
|
-
if (!res.ok) {
|
|
121
|
-
const errText = await res.text().catch(() => '');
|
|
122
|
-
throw new Error(`Backend ${method} ${pathname} returned ${res.status}: ${errText}`);
|
|
123
|
-
}
|
|
124
|
-
const text = await res.text();
|
|
125
|
-
return text ? JSON.parse(text) : null;
|
|
126
|
-
}
|
|
127
10
|
const server = new Server({ name: 'kobo-tasks', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
};
|
|
149
|
-
const TASK_ORDER_PROPERTIES = {
|
|
150
|
-
sort_order: {
|
|
151
|
-
type: 'integer',
|
|
152
|
-
minimum: 0,
|
|
153
|
-
description: 'Insert/move at this order, shifting siblings. Omit to append on creation. Mutually exclusive with after_task_id.',
|
|
154
|
-
},
|
|
155
|
-
after_task_id: {
|
|
156
|
-
type: 'string',
|
|
157
|
-
description: 'Insert/move immediately after this task in the same workspace, preserving the remaining order.',
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
const WORKSPACE_SCOPED_TOOLS = [
|
|
161
|
-
{
|
|
162
|
-
name: 'submit_final_review',
|
|
163
|
-
description: 'Submit the final structured verdict for this backend-owned auto-loop review, then end your turn. Include all actionable findings; use an empty findings array only when the entire mission is clear.',
|
|
164
|
-
inputSchema: {
|
|
165
|
-
type: 'object',
|
|
166
|
-
properties: {
|
|
167
|
-
summary: { type: 'string', minLength: 1, maxLength: 12000 },
|
|
168
|
-
findings: {
|
|
169
|
-
type: 'array',
|
|
170
|
-
maxItems: 100,
|
|
171
|
-
items: {
|
|
172
|
-
type: 'object',
|
|
173
|
-
properties: {
|
|
174
|
-
severity: { type: 'string', enum: ['critical', 'important', 'minor'] },
|
|
175
|
-
file: { type: 'string', minLength: 1, maxLength: 1000 },
|
|
176
|
-
line: { type: 'integer', minimum: 1 },
|
|
177
|
-
description: { type: 'string', minLength: 1, maxLength: 4000 },
|
|
178
|
-
recommendation: { type: 'string', minLength: 1, maxLength: 4000 },
|
|
179
|
-
},
|
|
180
|
-
required: ['severity', 'file', 'description', 'recommendation'],
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
required: ['summary', 'findings'],
|
|
185
|
-
},
|
|
186
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
name: 'submit_session_handoff',
|
|
190
|
-
description: 'Submit the final Markdown handoff requested by Kōbō for this generation turn, then end your turn. Only available during a backend-owned session transfer; does not launch or stop sessions.',
|
|
191
|
-
inputSchema: {
|
|
192
|
-
type: 'object',
|
|
193
|
-
properties: { report: { type: 'string', minLength: 1, maxLength: 24000 } },
|
|
194
|
-
required: ['report'],
|
|
195
|
-
},
|
|
196
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
name: 'list_tasks',
|
|
200
|
-
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, status, sort_order, role, and verification evidence. 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.',
|
|
201
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
202
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
name: 'mark_task_done',
|
|
206
|
-
description: 'CALL AS SOON AS a task or acceptance criterion is finished AND verified by checks actually performed. Auto-loop requires structured verification evidence with all checks passed. Never mark done if required checks failed or were not run. Finalization also requires all work tasks and criteria done. 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.',
|
|
207
|
-
inputSchema: {
|
|
208
|
-
type: 'object',
|
|
209
|
-
properties: {
|
|
210
|
-
task_id: { type: 'string', description: 'Task id from list_tasks.' },
|
|
211
|
-
verification: TASK_VERIFICATION_SCHEMA,
|
|
212
|
-
},
|
|
213
|
-
required: ['task_id'],
|
|
214
|
-
},
|
|
215
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
name: 'mark_auto_loop_ready',
|
|
219
|
-
description: 'CALL at the end of authorized auto-loop preparation, either during initial workspace creation with auto-loop or a later `/kobo-prep-autoloop` grooming session. First ensure tasks are atomic, implementable, and include the required verification/finalization work. Marks preparation ready; this is a mutation and cannot be called from a read-only launch. Do NOT call during unrelated normal sessions.',
|
|
220
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
221
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
name: 'create_task',
|
|
225
|
-
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 by default; use after_task_id for a dependent check immediately after its parent. Use role finalization for the final verification gate. Do not use it for ephemeral internal notes — prefer log_thought for those.',
|
|
226
|
-
inputSchema: {
|
|
227
|
-
type: 'object',
|
|
228
|
-
properties: {
|
|
229
|
-
...TASK_ORDER_PROPERTIES,
|
|
230
|
-
role: {
|
|
231
|
-
type: 'string',
|
|
232
|
-
enum: ['work', 'finalization'],
|
|
233
|
-
description: 'Default work. Finalization is the final verification gate and cannot finish while work/criteria remain open.',
|
|
234
|
-
},
|
|
235
|
-
title: { type: 'string', description: 'Short, imperative title (e.g. "Add retry to fetchUser").' },
|
|
236
|
-
is_acceptance_criterion: {
|
|
237
|
-
type: 'boolean',
|
|
238
|
-
description: 'Mark as acceptance criterion rather than a task (default: false).',
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
|
-
required: ['title'],
|
|
242
|
-
},
|
|
243
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
name: 'update_task',
|
|
247
|
-
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.',
|
|
248
|
-
inputSchema: {
|
|
249
|
-
type: 'object',
|
|
250
|
-
properties: {
|
|
251
|
-
task_id: { type: 'string', description: 'Task id from list_tasks.' },
|
|
252
|
-
...TASK_ORDER_PROPERTIES,
|
|
253
|
-
verification: TASK_VERIFICATION_SCHEMA,
|
|
254
|
-
title: { type: 'string', description: 'New title (optional).' },
|
|
255
|
-
status: {
|
|
256
|
-
type: 'string',
|
|
257
|
-
enum: ['pending', 'in_progress', 'done'],
|
|
258
|
-
description: 'New status (optional).',
|
|
259
|
-
},
|
|
260
|
-
is_acceptance_criterion: {
|
|
261
|
-
type: 'boolean',
|
|
262
|
-
description: 'Toggle acceptance criterion flag (optional).',
|
|
263
|
-
},
|
|
264
|
-
},
|
|
265
|
-
required: ['task_id'],
|
|
266
|
-
},
|
|
267
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
name: 'delete_task',
|
|
271
|
-
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.',
|
|
272
|
-
inputSchema: {
|
|
273
|
-
type: 'object',
|
|
274
|
-
properties: {
|
|
275
|
-
task_id: { type: 'string', description: 'Task id from list_tasks.' },
|
|
276
|
-
},
|
|
277
|
-
required: ['task_id'],
|
|
278
|
-
},
|
|
279
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
name: 'get_workspace_info',
|
|
283
|
-
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.',
|
|
284
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
285
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
name: 'set_auto_loop',
|
|
289
|
-
description: 'Enable or disable auto-loop for THIS workspace. Read get_workspace_info first. Enable it only when the user explicitly asks for autonomous continuation and pending tasks remain; disable it only when the user explicitly asks. Kōbō resumes the next iteration automatically after a session ends while it is enabled.',
|
|
290
|
-
inputSchema: {
|
|
291
|
-
type: 'object',
|
|
292
|
-
properties: {
|
|
293
|
-
enabled: {
|
|
294
|
-
type: 'boolean',
|
|
295
|
-
description: 'True to enable auto-loop; false to disable it.',
|
|
296
|
-
},
|
|
297
|
-
},
|
|
298
|
-
required: ['enabled'],
|
|
299
|
-
},
|
|
300
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
name: 'set_workspace_agent_description',
|
|
304
|
-
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.",
|
|
305
|
-
inputSchema: {
|
|
306
|
-
type: 'object',
|
|
307
|
-
properties: {
|
|
308
|
-
description: {
|
|
309
|
-
type: 'string',
|
|
310
|
-
description: 'Plain text, max 200 characters. Empty string clears the description.',
|
|
311
|
-
},
|
|
312
|
-
},
|
|
313
|
-
required: ['description'],
|
|
314
|
-
},
|
|
315
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
name: 'set_workspace_name',
|
|
319
|
-
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`.',
|
|
320
|
-
inputSchema: {
|
|
321
|
-
type: 'object',
|
|
322
|
-
properties: {
|
|
323
|
-
name: {
|
|
324
|
-
type: 'string',
|
|
325
|
-
description: 'New workspace name. Non-empty after trimming; control characters are stripped.',
|
|
326
|
-
},
|
|
327
|
-
},
|
|
328
|
-
required: ['name'],
|
|
329
|
-
},
|
|
330
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
331
|
-
},
|
|
332
|
-
{
|
|
333
|
-
name: 'cron_create',
|
|
334
|
-
description: 'Schedule a recurring trigger on THIS workspace. At each fire, Kōbō checks that the workspace is idle (no active session), then injects `prompt` into the conversation pinned when scheduled (`mode=resume`) or starts a new session (`mode=fresh`). 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.',
|
|
335
|
-
inputSchema: {
|
|
336
|
-
type: 'object',
|
|
337
|
-
properties: {
|
|
338
|
-
expression: {
|
|
339
|
-
type: 'string',
|
|
340
|
-
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.',
|
|
341
|
-
},
|
|
342
|
-
prompt: {
|
|
343
|
-
type: 'string',
|
|
344
|
-
description: 'The prompt to inject as the next user message at each fire.',
|
|
345
|
-
},
|
|
346
|
-
label: {
|
|
347
|
-
type: 'string',
|
|
348
|
-
description: 'Optional human-readable label for the cron (shown in the UI).',
|
|
349
|
-
},
|
|
350
|
-
mode: {
|
|
351
|
-
type: 'string',
|
|
352
|
-
enum: ['resume', 'fresh'],
|
|
353
|
-
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.",
|
|
354
|
-
},
|
|
355
|
-
oneShot: {
|
|
356
|
-
type: 'boolean',
|
|
357
|
-
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.",
|
|
358
|
-
},
|
|
359
|
-
},
|
|
360
|
-
required: ['expression', 'prompt'],
|
|
361
|
-
},
|
|
362
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
363
|
-
},
|
|
364
|
-
{
|
|
365
|
-
name: 'cron_delete',
|
|
366
|
-
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).",
|
|
367
|
-
inputSchema: {
|
|
368
|
-
type: 'object',
|
|
369
|
-
properties: {
|
|
370
|
-
id: { type: 'string', description: 'The cron id returned by cron_create.' },
|
|
371
|
-
},
|
|
372
|
-
required: ['id'],
|
|
373
|
-
},
|
|
374
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
name: 'cron_list',
|
|
378
|
-
description: 'List all crons currently armed on THIS workspace, including their next and last fire times.',
|
|
379
|
-
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
380
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
name: 'get_git_info',
|
|
384
|
-
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.',
|
|
385
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
386
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
name: 'set_workspace_status',
|
|
390
|
-
description: 'CALL WHEN you believe the mission is done (`completed`) or blocked beyond recovery (`error`). Do NOT set `idle` while this agent session is active: ask the user with AskUserQuestion or end the turn instead. Transitions are validated by the backend — invalid or unsafe ones are rejected.',
|
|
391
|
-
inputSchema: {
|
|
392
|
-
type: 'object',
|
|
393
|
-
properties: {
|
|
394
|
-
status: {
|
|
395
|
-
type: 'string',
|
|
396
|
-
enum: ['idle', 'completed', 'error'],
|
|
397
|
-
description: 'Target status.',
|
|
398
|
-
},
|
|
399
|
-
},
|
|
400
|
-
required: ['status'],
|
|
401
|
-
},
|
|
402
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
403
|
-
},
|
|
404
|
-
{
|
|
405
|
-
name: 'get_ticket',
|
|
406
|
-
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.',
|
|
407
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
408
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
409
|
-
},
|
|
410
|
-
{
|
|
411
|
-
name: 'get_dev_server_status',
|
|
412
|
-
description: 'CALL BEFORE backend, frontend, integration or E2E tests that need workspace services, or before asking the user whether the app is running. Reuse the existing instance and its returned URL/container names. Returns `not_configured` when this project has no dev server: report the blocker, do not launch one manually. Otherwise returns running/stopped/starting/error + URL, port, container names. Running is lifecycle status, not application readiness: use the documented health check with bounded retries before dependent tests.',
|
|
413
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
414
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
415
|
-
},
|
|
416
|
-
{
|
|
417
|
-
name: 'start_dev_server',
|
|
418
|
-
description: 'CALL WHEN an authorized task needs workspace services for app checks or backend/frontend/integration/E2E tests and get_dev_server_status reports stopped. Kōbō runs the configured startup command and tracks the instance. Use this tool instead of shell lifecycle commands such as make dev-docker, sh/dev-docker.sh, docker compose up or npm run dev. Respect launch permissions; read-only sessions cannot start services. On failure, inspect get_dev_server_logs and report the blocker, without a manual shell fallback.',
|
|
419
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
420
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
421
|
-
},
|
|
422
|
-
{
|
|
423
|
-
name: 'stop_dev_server',
|
|
424
|
-
description: 'CALL WHEN the user explicitly asks to stop the dev server, or when an authorized operation requires a restart. Use this tool instead of shell shutdown commands such as docker compose down or killing the server process directly. Respect launch permissions; read-only sessions cannot stop services. Do not stop a shared workspace instance merely because your tests finished.',
|
|
425
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
426
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
427
|
-
},
|
|
428
|
-
{
|
|
429
|
-
name: 'get_dev_server_logs',
|
|
430
|
-
description: 'CALL WHEN diagnosing a dev-server startup failure or a runtime issue during app checks or tests. Returns the last N lines of logs (default 200). Use these logs with get_dev_server_status before reporting a blocker; do not bypass a failed managed startup with a shell launch.',
|
|
431
|
-
inputSchema: {
|
|
432
|
-
type: 'object',
|
|
433
|
-
properties: {
|
|
434
|
-
tail: { type: 'number', description: 'Number of lines from the end (default: 200).' },
|
|
435
|
-
},
|
|
436
|
-
required: [],
|
|
437
|
-
},
|
|
438
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
439
|
-
},
|
|
440
|
-
{
|
|
441
|
-
name: 'list_workspace_images',
|
|
442
|
-
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/.',
|
|
443
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
444
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
445
|
-
},
|
|
446
|
-
{
|
|
447
|
-
name: 'get_settings',
|
|
448
|
-
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.',
|
|
449
|
-
inputSchema: {
|
|
450
|
-
type: 'object',
|
|
451
|
-
properties: {
|
|
452
|
-
project_path: {
|
|
453
|
-
type: 'string',
|
|
454
|
-
description: 'Project path to resolve a specific project entry (optional).',
|
|
455
|
-
},
|
|
456
|
-
},
|
|
457
|
-
required: [],
|
|
458
|
-
},
|
|
459
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
460
|
-
},
|
|
461
|
-
// ── Knowledge / context tools ─────────────────────────────────────────────
|
|
462
|
-
{
|
|
463
|
-
name: 'list_documents',
|
|
464
|
-
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/, .ai/thoughts/, and .ai/handoffs/. Before writing a new plan, check if one already exists.',
|
|
465
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
466
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
467
|
-
},
|
|
468
|
-
{
|
|
469
|
-
name: 'read_document',
|
|
470
|
-
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/, .ai/handoffs/ — reject anything else.',
|
|
471
|
-
inputSchema: {
|
|
472
|
-
type: 'object',
|
|
473
|
-
properties: {
|
|
474
|
-
path: {
|
|
475
|
-
type: 'string',
|
|
476
|
-
description: 'Worktree-relative path from list_documents (e.g. "docs/superpowers/plans/2026-04-17-foo.md").',
|
|
477
|
-
},
|
|
478
|
-
},
|
|
479
|
-
required: ['path'],
|
|
480
|
-
},
|
|
481
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
482
|
-
},
|
|
483
|
-
{
|
|
484
|
-
name: 'log_thought',
|
|
485
|
-
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.',
|
|
486
|
-
inputSchema: {
|
|
487
|
-
type: 'object',
|
|
488
|
-
properties: {
|
|
489
|
-
title: { type: 'string', description: 'Short, descriptive title (becomes the filename slug and the # H1).' },
|
|
490
|
-
content: { type: 'string', description: 'Markdown body explaining the decision and its reasoning.' },
|
|
491
|
-
tag: {
|
|
492
|
-
type: 'string',
|
|
493
|
-
description: 'Optional short tag appended to filename (e.g. "arch", "bug", "perf").',
|
|
494
|
-
},
|
|
495
|
-
},
|
|
496
|
-
required: ['title', 'content'],
|
|
497
|
-
},
|
|
498
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
499
|
-
},
|
|
500
|
-
{
|
|
501
|
-
name: 'search_codebase',
|
|
502
|
-
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.',
|
|
503
|
-
inputSchema: {
|
|
504
|
-
type: 'object',
|
|
505
|
-
properties: {
|
|
506
|
-
query: { type: 'string', description: 'Search phrase. Plain text; no regex.' },
|
|
507
|
-
include_archived: {
|
|
508
|
-
type: 'boolean',
|
|
509
|
-
description: 'Include archived workspaces in the search (default: false).',
|
|
510
|
-
},
|
|
511
|
-
scope: {
|
|
512
|
-
type: 'string',
|
|
513
|
-
enum: ['workspace', 'all'],
|
|
514
|
-
description: 'Restrict to this workspace only (default) or search across every workspace.',
|
|
515
|
-
},
|
|
516
|
-
limit: { type: 'number', description: 'Max results to return (default 30, max 100).' },
|
|
517
|
-
},
|
|
518
|
-
required: ['query'],
|
|
519
|
-
},
|
|
520
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
521
|
-
},
|
|
522
|
-
{
|
|
523
|
-
name: 'get_session_usage',
|
|
524
|
-
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.',
|
|
525
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
526
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
527
|
-
},
|
|
528
|
-
{
|
|
529
|
-
name: 'read_workspace_events_csv',
|
|
530
|
-
description: 'Read the user/agent conversation history for THIS workspace as paginated CSV. Use it to recover context from prior sessions without loading the whole history at once. Optionally filter to one session_id. Read-only; cannot access another workspace.',
|
|
531
|
-
inputSchema: {
|
|
532
|
-
type: 'object',
|
|
533
|
-
properties: {
|
|
534
|
-
session_id: { type: 'string', description: 'Optional Kōbō session id to restrict the history.' },
|
|
535
|
-
offset: { type: 'number', description: 'Zero-based offset for pagination, default 0.' },
|
|
536
|
-
limit: { type: 'number', description: 'Messages to return, default 100, max 500.' },
|
|
537
|
-
},
|
|
538
|
-
required: [],
|
|
539
|
-
},
|
|
540
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
541
|
-
},
|
|
542
|
-
{
|
|
543
|
-
name: 'schedule_wakeup',
|
|
544
|
-
description: 'WHEN: the parent agent needs a durable later check for detached work or external state without a tracked completion notification. Do not duplicate SDK tracked-job continuation or schedule merely to advance auto-loop; subagents must leave the one workspace wakeup to their parent. Delay is measured from this call and clamped to [60, 21600] seconds. The wakeup targets this workspace and the session selected at scheduling time, not whichever session is newest later. It is deferred while busy when it cannot be delivered to the waiting controller; active auto-loop receives it as a queued instruction. Replaces the existing pending wakeup on this workspace, so preserve unrelated scheduled intent. Supply a concrete follow-up prompt and optional reason. End the current turn normally; delivery timing also depends on workspace availability. Prefer this durable Kōbō tool over native ScheduleWakeup',
|
|
545
|
-
inputSchema: {
|
|
546
|
-
type: 'object',
|
|
547
|
-
properties: {
|
|
548
|
-
delaySeconds: {
|
|
549
|
-
type: 'number',
|
|
550
|
-
description: 'Seconds from now until the wakeup fires. Clamped to [60, 21600] (1min to 6h).',
|
|
551
|
-
},
|
|
552
|
-
prompt: {
|
|
553
|
-
type: 'string',
|
|
554
|
-
description: 'Prompt sent to the agent when the wakeup fires.',
|
|
555
|
-
},
|
|
556
|
-
reason: {
|
|
557
|
-
type: 'string',
|
|
558
|
-
description: 'Short label shown to the user explaining the wakeup (optional).',
|
|
559
|
-
},
|
|
560
|
-
},
|
|
561
|
-
required: ['delaySeconds', 'prompt'],
|
|
562
|
-
},
|
|
563
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
564
|
-
},
|
|
565
|
-
{
|
|
566
|
-
name: 'cancel_wakeup',
|
|
567
|
-
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.',
|
|
568
|
-
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
569
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
570
|
-
},
|
|
571
|
-
...MEMORY_TOOL_DEFINITIONS,
|
|
572
|
-
...INTERNAL_PROGRESS_BAR_TOOLS,
|
|
573
|
-
];
|
|
574
|
-
const GLOBAL_TOOLS = [
|
|
575
|
-
...WORKSPACE_DIALOGUE_TOOLS,
|
|
576
|
-
...WORKSPACE_CREATION_TOOLS,
|
|
577
|
-
...WORKSPACE_LIFECYCLE_TOOLS,
|
|
578
|
-
...WORKSPACE_GROUP_MESSAGE_TOOLS,
|
|
579
|
-
...EXTERNAL_MEMORY_TOOL_DEFINITIONS,
|
|
580
|
-
...PROGRESS_BAR_TOOLS,
|
|
581
|
-
{
|
|
582
|
-
name: 'list_workspaces',
|
|
583
|
-
description: 'List Kōbō workspaces with their id, title, status, tags, and creation date. Set tag to return only workspaces carrying that exact tag. 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.',
|
|
584
|
-
inputSchema: {
|
|
585
|
-
type: 'object',
|
|
586
|
-
properties: {
|
|
587
|
-
include_archived: {
|
|
588
|
-
type: 'boolean',
|
|
589
|
-
description: 'Include archived workspaces (default false).',
|
|
590
|
-
},
|
|
591
|
-
tag: {
|
|
592
|
-
type: 'string',
|
|
593
|
-
minLength: 1,
|
|
594
|
-
description: 'Return only workspaces with this exact tag, for example "API".',
|
|
595
|
-
},
|
|
596
|
-
},
|
|
597
|
-
required: [],
|
|
598
|
-
},
|
|
599
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
600
|
-
},
|
|
601
|
-
{
|
|
602
|
-
name: 'stop_workspace',
|
|
603
|
-
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.',
|
|
604
|
-
inputSchema: {
|
|
605
|
-
type: 'object',
|
|
606
|
-
properties: {
|
|
607
|
-
workspace_id: { type: 'string', description: 'Workspace id, from list_workspaces.' },
|
|
608
|
-
},
|
|
609
|
-
required: ['workspace_id'],
|
|
610
|
-
},
|
|
611
|
-
annotations: { destructiveHint: false, openWorldHint: false },
|
|
612
|
-
},
|
|
613
|
-
];
|
|
614
|
-
/**
|
|
615
|
-
* Tool names callable without KOBO_WORKSPACE_ID set. Everything else requires
|
|
616
|
-
* a workspace-bound session — enforced once, up front, in the
|
|
617
|
-
* CallToolRequestSchema handler below rather than repeating a guard in every
|
|
618
|
-
* one of the ~35 workspace-scoped branches.
|
|
619
|
-
*/
|
|
620
|
-
const GLOBAL_TOOL_NAMES = new Set(GLOBAL_TOOLS.map((t) => t.name));
|
|
621
|
-
function availableTools() {
|
|
622
|
-
// A workspace process exposes the capability-bound memory tools exactly once.
|
|
623
|
-
// Unbound/global stdio exposes the external bridge variant instead.
|
|
624
|
-
const tools = workspaceId
|
|
625
|
-
? [
|
|
626
|
-
...WORKSPACE_SCOPED_TOOLS,
|
|
627
|
-
...GLOBAL_TOOLS.filter((tool) => !isMemoryToolName(tool.name) && !isProgressBarTool(tool.name)),
|
|
628
|
-
]
|
|
629
|
-
: GLOBAL_TOOLS;
|
|
630
|
-
return tools.filter((tool) => handoffId && handoffToken
|
|
631
|
-
? tool.name === 'submit_session_handoff' ||
|
|
632
|
-
tool.annotations?.readOnlyHint === true ||
|
|
633
|
-
(isProgressBarTool(tool.name) && !!process.env.KOBO_PROGRESS_SESSION_TOKEN)
|
|
634
|
-
: finalReviewToken && finalReviewSessionId
|
|
635
|
-
? tool.name === 'submit_final_review' ||
|
|
636
|
-
tool.annotations?.readOnlyHint === true ||
|
|
637
|
-
(isProgressBarTool(tool.name) && !!process.env.KOBO_PROGRESS_SESSION_TOKEN)
|
|
638
|
-
: tool.name !== 'submit_session_handoff' && tool.name !== 'submit_final_review');
|
|
639
|
-
}
|
|
640
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: availableTools() }));
|
|
641
|
-
/** Wrap a successful result as an MCP tool response with JSON text content. */
|
|
642
|
-
function ok(data) {
|
|
643
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
644
|
-
}
|
|
645
|
-
/** Wrap an error message as an MCP tool error response. */
|
|
646
|
-
function fail(message) {
|
|
647
|
-
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
648
|
-
}
|
|
649
|
-
/**
|
|
650
|
-
* Turn a backendRequest() rejection into a user-facing message. backendRequest
|
|
651
|
-
* always throws `Backend ${method} ${pathname} returned ${status}: ...` for a
|
|
652
|
-
* non-2xx HTTP response (already a clear message — pass it through). Any
|
|
653
|
-
* other error (fetch itself throwing, e.g. ECONNREFUSED) means the backend
|
|
654
|
-
* process isn't reachable at all, which gets a friendlier, actionable message.
|
|
655
|
-
*/
|
|
656
|
-
function backendErrorMessage(err) {
|
|
657
|
-
if (err instanceof Error && err.message.startsWith('Backend ')) {
|
|
658
|
-
return err.message;
|
|
659
|
-
}
|
|
660
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
661
|
-
return `Kōbō backend unreachable at ${backendUrl} — is the app running? (${detail})`;
|
|
662
|
-
}
|
|
11
|
+
/** Managed processes never open SQLite or possess the administrator token. */
|
|
12
|
+
async function managedRequest(body) {
|
|
13
|
+
if (!capability)
|
|
14
|
+
throw new Error('Workspace tools require a current managed MCP launch capability');
|
|
15
|
+
const response = await fetch(`${backendUrl.replace(/\/$/, '')}/api/internal-mcp`, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
redirect: 'error',
|
|
18
|
+
headers: { 'Content-Type': 'application/json', 'X-Kobo-Mcp-Session': capability },
|
|
19
|
+
body: JSON.stringify({ ...body, clientName: process.env.KOBO_MCP_CLIENT_NAME ?? server.getClientVersion()?.name }),
|
|
20
|
+
signal: AbortSignal.timeout(15 * 60_000),
|
|
21
|
+
});
|
|
22
|
+
const result = await response.json();
|
|
23
|
+
if (!response.ok && body.method === 'list')
|
|
24
|
+
throw new Error('Managed MCP capability rejected');
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
28
|
+
if (workspaceId)
|
|
29
|
+
return managedRequest({ method: 'list' });
|
|
30
|
+
return listWorkspaceDialogueTools(backendUrl, process.env.KOBO_MCP_TOKEN, process.env.KOBO_MCP_CLIENT_NAME ?? server.getClientVersion()?.name);
|
|
31
|
+
});
|
|
663
32
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
664
|
-
const { name, arguments: args } = request.params;
|
|
665
|
-
const a = (args ?? {});
|
|
666
|
-
if (!workspaceId && !GLOBAL_TOOL_NAMES.has(name)) {
|
|
667
|
-
return fail(`Tool '${name}' requires a workspace-bound MCP session (KOBO_WORKSPACE_ID not set).`);
|
|
668
|
-
}
|
|
669
33
|
try {
|
|
670
|
-
if (
|
|
671
|
-
return
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
return ok(await backendRequest('POST', `/api/workspaces/${workspaceId}/auto-loop/final-review/report`, {
|
|
676
|
-
token: finalReviewToken,
|
|
677
|
-
sessionId: finalReviewSessionId,
|
|
678
|
-
report: a,
|
|
679
|
-
}));
|
|
680
|
-
}
|
|
681
|
-
if (handoffId && handoffToken && !availableTools().some((tool) => tool.name === name))
|
|
682
|
-
return fail('This turn is reserved for a handoff report. Only read tools and submit_session_handoff are available.');
|
|
683
|
-
if (name === 'submit_session_handoff') {
|
|
684
|
-
if (!workspaceId || !handoffId || !handoffToken)
|
|
685
|
-
return fail('No handoff generation is active for this MCP invocation.');
|
|
686
|
-
return ok(await backendRequest('POST', `/api/workspaces/${workspaceId}/session-handoffs/${handoffId}/report`, {
|
|
687
|
-
token: handoffToken,
|
|
688
|
-
report: a.report,
|
|
689
|
-
}));
|
|
690
|
-
}
|
|
691
|
-
if (isProgressBarTool(name)) {
|
|
692
|
-
if (!workspaceId)
|
|
693
|
-
return await callWorkspaceDialogueTool(backendUrl, name, a, process.env.KOBO_NETWORK_TOKEN, process.env.KOBO_MCP_CLIENT_NAME ?? server.getClientVersion()?.name);
|
|
694
|
-
return await callProgressBarTool({
|
|
695
|
-
backendUrl,
|
|
696
|
-
name,
|
|
697
|
-
args: a,
|
|
698
|
-
capability: process.env.KOBO_PROGRESS_SESSION_TOKEN,
|
|
699
|
-
networkToken: process.env.KOBO_NETWORK_TOKEN,
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
if (isMemoryToolName(name)) {
|
|
703
|
-
if (!workspaceId) {
|
|
704
|
-
return await callWorkspaceDialogueTool(backendUrl, name, a, process.env.KOBO_NETWORK_TOKEN, process.env.KOBO_MCP_CLIENT_NAME ?? server.getClientVersion()?.name);
|
|
705
|
-
}
|
|
706
|
-
// Workspace-bound memory is always dispatched to its launch-scoped capability.
|
|
707
|
-
// It must never fall through to the unrestricted external/global bridge.
|
|
708
|
-
if (!process.env.KOBO_MEMORY_SESSION_TOKEN)
|
|
709
|
-
return fail('Memory tools require the current workspace launch capability.');
|
|
710
|
-
const memoryResult = await callMemoryTool({
|
|
711
|
-
backendUrl,
|
|
712
|
-
name,
|
|
713
|
-
args: a,
|
|
714
|
-
capability: process.env.KOBO_MEMORY_SESSION_TOKEN,
|
|
715
|
-
networkToken: process.env.KOBO_NETWORK_TOKEN,
|
|
716
|
-
});
|
|
717
|
-
if (isMemoryOutputSuppressed(memoryResult))
|
|
718
|
-
return { content: [], isError: true };
|
|
719
|
-
return {
|
|
720
|
-
...ok(memoryResult),
|
|
721
|
-
...(memoryResult && typeof memoryResult === 'object' && 'budgetExhausted' in memoryResult
|
|
722
|
-
? { isError: true }
|
|
723
|
-
: {}),
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
if (isWorkspaceCreationTool(name) ||
|
|
727
|
-
isWorkspaceLifecycleTool(name) ||
|
|
728
|
-
isWorkspaceGroupMessageTool(name) ||
|
|
729
|
-
WORKSPACE_DIALOGUE_TOOLS.some((tool) => tool.name === name)) {
|
|
730
|
-
if (workspaceId &&
|
|
731
|
-
((a.workspace_id === workspaceId && name === 'send_workspace_message') ||
|
|
732
|
-
(name === 'send_workspace_group_message' &&
|
|
733
|
-
Array.isArray(a.workspace_ids) &&
|
|
734
|
-
a.workspace_ids.includes(workspaceId))))
|
|
735
|
-
return fail('Use the normal conversation to communicate in your own workspace.');
|
|
736
|
-
return await callWorkspaceDialogueTool(backendUrl, name, a, process.env.KOBO_NETWORK_TOKEN, process.env.KOBO_MCP_CLIENT_NAME ?? server.getClientVersion()?.name);
|
|
737
|
-
}
|
|
738
|
-
if (name === 'list_tasks') {
|
|
739
|
-
return ok(listTasksHandler(db, workspaceId));
|
|
740
|
-
}
|
|
741
|
-
if (name === 'mark_task_done') {
|
|
742
|
-
const taskId = a.task_id;
|
|
743
|
-
if (!taskId)
|
|
744
|
-
return fail('task_id parameter is required');
|
|
745
|
-
const result = markTaskDoneHandler(db, workspaceId, taskId, a.verification);
|
|
746
|
-
void notifyBackend(taskId);
|
|
747
|
-
return ok(result);
|
|
748
|
-
}
|
|
749
|
-
if (name === 'mark_auto_loop_ready') {
|
|
750
|
-
const result = markAutoLoopReadyHandler(db, workspaceId);
|
|
751
|
-
void notifyAutoLoopReady();
|
|
752
|
-
return ok(result);
|
|
753
|
-
}
|
|
754
|
-
if (name === 'create_task') {
|
|
755
|
-
const title = a.title;
|
|
756
|
-
if (!title)
|
|
757
|
-
return fail('title parameter is required');
|
|
758
|
-
const task = createTaskHandler(db, workspaceId, {
|
|
759
|
-
title,
|
|
760
|
-
role: a.role,
|
|
761
|
-
is_acceptance_criterion: a.is_acceptance_criterion,
|
|
762
|
-
sort_order: a.sort_order,
|
|
763
|
-
after_task_id: a.after_task_id,
|
|
34
|
+
if (workspaceId)
|
|
35
|
+
return await managedRequest({
|
|
36
|
+
method: 'call',
|
|
37
|
+
name: request.params.name,
|
|
38
|
+
arguments: request.params.arguments ?? {},
|
|
764
39
|
});
|
|
765
|
-
|
|
766
|
-
return ok(task);
|
|
767
|
-
}
|
|
768
|
-
if (name === 'update_task') {
|
|
769
|
-
const taskId = a.task_id;
|
|
770
|
-
if (!taskId)
|
|
771
|
-
return fail('task_id parameter is required');
|
|
772
|
-
const task = updateTaskHandler(db, workspaceId, taskId, {
|
|
773
|
-
title: a.title,
|
|
774
|
-
status: a.status,
|
|
775
|
-
verification: a.verification,
|
|
776
|
-
is_acceptance_criterion: a.is_acceptance_criterion,
|
|
777
|
-
sort_order: a.sort_order,
|
|
778
|
-
after_task_id: a.after_task_id,
|
|
779
|
-
});
|
|
780
|
-
void notifyTasksUpdated();
|
|
781
|
-
return ok(task);
|
|
782
|
-
}
|
|
783
|
-
if (name === 'delete_task') {
|
|
784
|
-
const taskId = a.task_id;
|
|
785
|
-
if (!taskId)
|
|
786
|
-
return fail('task_id parameter is required');
|
|
787
|
-
const result = deleteTaskHandler(db, workspaceId, taskId);
|
|
788
|
-
void notifyTasksUpdated();
|
|
789
|
-
return ok(result);
|
|
790
|
-
}
|
|
791
|
-
if (name === 'get_settings') {
|
|
792
|
-
return ok(getSettingsHandler(settingsPath, a.project_path));
|
|
793
|
-
}
|
|
794
|
-
if (name === 'get_dev_server_status') {
|
|
795
|
-
try {
|
|
796
|
-
const result = await backendRequest('GET', `/api/dev-server/${workspaceId}/status`);
|
|
797
|
-
return ok(result);
|
|
798
|
-
}
|
|
799
|
-
catch {
|
|
800
|
-
// Fallback to DB if the backend HTTP API is unreachable
|
|
801
|
-
return ok(getDevServerStatusHandler(db, workspaceId));
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
if (name === 'get_workspace_info') {
|
|
805
|
-
return ok(getWorkspaceInfoHandler(db, workspaceId));
|
|
806
|
-
}
|
|
807
|
-
if (name === 'set_auto_loop') {
|
|
808
|
-
if (typeof a.enabled !== 'boolean')
|
|
809
|
-
return fail('enabled parameter is required and must be boolean');
|
|
810
|
-
try {
|
|
811
|
-
const method = a.enabled ? 'POST' : 'DELETE';
|
|
812
|
-
const result = await backendRequest(method, `/api/workspaces/${workspaceId}/auto-loop`);
|
|
813
|
-
return ok(result);
|
|
814
|
-
}
|
|
815
|
-
catch (err) {
|
|
816
|
-
return fail(err instanceof Error ? err.message : String(err));
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
if (name === 'set_workspace_agent_description') {
|
|
820
|
-
const description = a.description;
|
|
821
|
-
if (typeof description !== 'string')
|
|
822
|
-
return fail('description parameter is required');
|
|
823
|
-
const result = setWorkspaceAgentDescriptionHandler(db, workspaceId, { description });
|
|
824
|
-
if ('ok' in result && result.ok) {
|
|
825
|
-
void notifyAgentDescriptionUpdated();
|
|
826
|
-
}
|
|
827
|
-
return ok(result);
|
|
828
|
-
}
|
|
829
|
-
if (name === 'set_workspace_name') {
|
|
830
|
-
const newName = a.name;
|
|
831
|
-
if (typeof newName !== 'string' || !newName.trim())
|
|
832
|
-
return fail('name parameter is required (non-empty)');
|
|
833
|
-
try {
|
|
834
|
-
const updated = (await backendRequest('PATCH', `/api/workspaces/${workspaceId}`, { name: newName }));
|
|
835
|
-
return ok({ ok: true, name: updated.name ?? newName.trim() });
|
|
836
|
-
}
|
|
837
|
-
catch (err) {
|
|
838
|
-
return fail(err instanceof Error ? err.message : String(err));
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
if (name === 'cron_create') {
|
|
842
|
-
const expression = a.expression;
|
|
843
|
-
const prompt = a.prompt;
|
|
844
|
-
const label = typeof a.label === 'string' ? a.label : undefined;
|
|
845
|
-
const mode = typeof a.mode === 'string' ? a.mode : undefined;
|
|
846
|
-
const oneShot = typeof a.oneShot === 'boolean' ? a.oneShot : undefined;
|
|
847
|
-
if (typeof expression !== 'string' || typeof prompt !== 'string') {
|
|
848
|
-
return fail('expression and prompt parameters are required');
|
|
849
|
-
}
|
|
850
|
-
if (mode !== undefined && mode !== 'resume' && mode !== 'fresh') {
|
|
851
|
-
return fail("mode must be 'resume' or 'fresh'");
|
|
852
|
-
}
|
|
853
|
-
// Route through the backend so the in-memory `setTimeout` lives in the
|
|
854
|
-
// backend process (which owns orchestrator + WS broadcast). Calling
|
|
855
|
-
// cron-service.arm() directly here would persist the row but arm the
|
|
856
|
-
// timer in the MCP server sub-process, which dies with the agent
|
|
857
|
-
// session — fires would never trigger a real session resume.
|
|
858
|
-
try {
|
|
859
|
-
const created = (await backendRequest('POST', `/api/workspaces/${workspaceId}/crons`, {
|
|
860
|
-
expression,
|
|
861
|
-
prompt,
|
|
862
|
-
label,
|
|
863
|
-
mode,
|
|
864
|
-
oneShot,
|
|
865
|
-
}));
|
|
866
|
-
return ok({ ok: true, cron: created.cron });
|
|
867
|
-
}
|
|
868
|
-
catch (err) {
|
|
869
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
870
|
-
return ok({ ok: false, error: message });
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
if (name === 'cron_delete') {
|
|
874
|
-
const id = a.id;
|
|
875
|
-
if (typeof id !== 'string')
|
|
876
|
-
return fail('id parameter is required');
|
|
877
|
-
// Same reason as cron_create — the backend owns the timer Map.
|
|
878
|
-
try {
|
|
879
|
-
await backendRequest('DELETE', `/api/workspaces/${workspaceId}/crons/${id}`);
|
|
880
|
-
return ok({ ok: true });
|
|
881
|
-
}
|
|
882
|
-
catch (err) {
|
|
883
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
884
|
-
return ok({ ok: false, error: message });
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
if (name === 'cron_list') {
|
|
888
|
-
const result = cronListHandler(db, workspaceId);
|
|
889
|
-
return ok(result);
|
|
890
|
-
}
|
|
891
|
-
if (name === 'start_dev_server') {
|
|
892
|
-
const result = await backendRequest('POST', `/api/dev-server/${workspaceId}/start`);
|
|
893
|
-
return ok(result);
|
|
894
|
-
}
|
|
895
|
-
if (name === 'stop_dev_server') {
|
|
896
|
-
const result = await backendRequest('POST', `/api/dev-server/${workspaceId}/stop`);
|
|
897
|
-
return ok(result);
|
|
898
|
-
}
|
|
899
|
-
if (name === 'get_dev_server_logs') {
|
|
900
|
-
const tail = a.tail ?? 200;
|
|
901
|
-
const result = await backendRequest('GET', `/api/dev-server/${workspaceId}/logs?tail=${tail}`);
|
|
902
|
-
return ok(result);
|
|
903
|
-
}
|
|
904
|
-
if (name === 'list_workspace_images') {
|
|
905
|
-
const info = getWorkspaceInfoHandler(db, workspaceId);
|
|
906
|
-
return ok(listWorkspaceImagesHandler(info.worktreePath));
|
|
907
|
-
}
|
|
908
|
-
// `get_notion_ticket` is the pre-1.7 name, kept as a back-compat alias so
|
|
909
|
-
// sessions resumed against an older tool list still resolve.
|
|
910
|
-
if (name === 'get_ticket' || name === 'get_notion_ticket') {
|
|
911
|
-
const info = getWorkspaceInfoHandler(db, workspaceId);
|
|
912
|
-
return ok({ sources: getTicketSourcesHandler(info.worktreePath) });
|
|
913
|
-
}
|
|
914
|
-
if (name === 'get_git_info') {
|
|
915
|
-
const result = await backendRequest('GET', `/api/workspaces/${workspaceId}/git-stats`);
|
|
916
|
-
return ok(result);
|
|
917
|
-
}
|
|
918
|
-
if (name === 'set_workspace_status') {
|
|
919
|
-
const status = a.status;
|
|
920
|
-
if (!status)
|
|
921
|
-
return fail('status parameter is required');
|
|
922
|
-
const result = await backendRequest('PATCH', `/api/workspaces/${workspaceId}`, { status });
|
|
923
|
-
return ok(result);
|
|
924
|
-
}
|
|
925
|
-
if (name === 'list_documents') {
|
|
926
|
-
const info = getWorkspaceInfoHandler(db, workspaceId);
|
|
927
|
-
return ok(listDocumentsHandler(info.worktreePath));
|
|
928
|
-
}
|
|
929
|
-
if (name === 'read_document') {
|
|
930
|
-
const docPath = a.path;
|
|
931
|
-
if (!docPath)
|
|
932
|
-
return fail('path parameter is required');
|
|
933
|
-
const info = getWorkspaceInfoHandler(db, workspaceId);
|
|
934
|
-
return ok(readDocumentHandler(info.worktreePath, docPath));
|
|
935
|
-
}
|
|
936
|
-
if (name === 'log_thought') {
|
|
937
|
-
const title = a.title;
|
|
938
|
-
const content = a.content;
|
|
939
|
-
if (!title)
|
|
940
|
-
return fail('title parameter is required');
|
|
941
|
-
if (!content)
|
|
942
|
-
return fail('content parameter is required');
|
|
943
|
-
const info = getWorkspaceInfoHandler(db, workspaceId);
|
|
944
|
-
return ok(logThoughtHandler(info.worktreePath, {
|
|
945
|
-
title,
|
|
946
|
-
content,
|
|
947
|
-
tag: a.tag,
|
|
948
|
-
}));
|
|
949
|
-
}
|
|
950
|
-
if (name === 'get_session_usage') {
|
|
951
|
-
return ok(getSessionUsageHandler(db, workspaceId));
|
|
952
|
-
}
|
|
953
|
-
if (name === 'read_workspace_events_csv') {
|
|
954
|
-
return ok(readWorkspaceEventsCsvHandler(db, workspaceId, {
|
|
955
|
-
sessionId: typeof a.session_id === 'string' ? a.session_id : undefined,
|
|
956
|
-
offset: typeof a.offset === 'number' ? a.offset : undefined,
|
|
957
|
-
limit: typeof a.limit === 'number' ? a.limit : undefined,
|
|
958
|
-
}));
|
|
959
|
-
}
|
|
960
|
-
if (name === 'schedule_wakeup') {
|
|
961
|
-
const delaySeconds = a.delaySeconds;
|
|
962
|
-
const prompt = a.prompt;
|
|
963
|
-
if (typeof delaySeconds !== 'number' || !Number.isFinite(delaySeconds) || delaySeconds <= 0) {
|
|
964
|
-
return fail('delaySeconds must be a positive number');
|
|
965
|
-
}
|
|
966
|
-
if (typeof prompt !== 'string' || prompt.trim().length === 0) {
|
|
967
|
-
return fail('prompt is required');
|
|
968
|
-
}
|
|
969
|
-
const reason = a.reason;
|
|
970
|
-
if (reason !== undefined && typeof reason !== 'string') {
|
|
971
|
-
return fail('reason must be a string when provided');
|
|
972
|
-
}
|
|
973
|
-
const result = await backendRequest('POST', `/api/workspaces/${workspaceId}/pending-wakeup`, {
|
|
974
|
-
delaySeconds,
|
|
975
|
-
prompt,
|
|
976
|
-
reason,
|
|
977
|
-
// Pin the calling session so the wakeup resumes THIS conversation (keeps
|
|
978
|
-
// context). The route now defaults to 'fresh' for manual UI scheduling;
|
|
979
|
-
// the agent must opt into 'resume' explicitly.
|
|
980
|
-
mode: 'resume',
|
|
981
|
-
});
|
|
982
|
-
return ok(result);
|
|
983
|
-
}
|
|
984
|
-
if (name === 'cancel_wakeup') {
|
|
985
|
-
const result = await backendRequest('DELETE', `/api/workspaces/${workspaceId}/pending-wakeup`);
|
|
986
|
-
return ok(result);
|
|
987
|
-
}
|
|
988
|
-
if (name === 'search_codebase') {
|
|
989
|
-
const query = a.query;
|
|
990
|
-
if (!query)
|
|
991
|
-
return fail('query parameter is required');
|
|
992
|
-
const scope = a.scope ?? 'workspace';
|
|
993
|
-
const includeArchived = a.include_archived === true;
|
|
994
|
-
const limit = Math.min(Math.max(1, a.limit ?? 30), 100);
|
|
995
|
-
const qs = new URLSearchParams({ q: query, limit: String(limit) });
|
|
996
|
-
if (includeArchived)
|
|
997
|
-
qs.set('includeArchived', 'true');
|
|
998
|
-
const raw = (await backendRequest('GET', `/api/search?${qs.toString()}`));
|
|
999
|
-
const results = scope === 'all' ? raw : raw.filter((r) => r.workspaceId === workspaceId);
|
|
1000
|
-
return ok({ query, scope, total: results.length, results });
|
|
1001
|
-
}
|
|
1002
|
-
if (name === 'list_workspaces') {
|
|
1003
|
-
const includeArchived = a.include_archived === true;
|
|
1004
|
-
const tag = typeof a.tag === 'string' ? a.tag : undefined;
|
|
1005
|
-
return ok(listWorkspacesHandler(db, { includeArchived, tag }));
|
|
1006
|
-
}
|
|
1007
|
-
if (name === 'stop_workspace') {
|
|
1008
|
-
const targetId = a.workspace_id;
|
|
1009
|
-
if (!targetId)
|
|
1010
|
-
return fail('workspace_id parameter is required');
|
|
1011
|
-
try {
|
|
1012
|
-
const result = await backendRequest('POST', `/api/workspaces/${targetId}/stop`);
|
|
1013
|
-
return ok(result);
|
|
1014
|
-
}
|
|
1015
|
-
catch (err) {
|
|
1016
|
-
return fail(backendErrorMessage(err));
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
return fail(`Unknown tool: ${name}`);
|
|
40
|
+
return await callWorkspaceDialogueTool(backendUrl, request.params.name, request.params.arguments ?? {}, process.env.KOBO_MCP_TOKEN, process.env.KOBO_MCP_CLIENT_NAME ?? server.getClientVersion()?.name);
|
|
1020
41
|
}
|
|
1021
|
-
catch (
|
|
1022
|
-
return
|
|
42
|
+
catch (error) {
|
|
43
|
+
return {
|
|
44
|
+
isError: true,
|
|
45
|
+
content: [{ type: 'text', text: error instanceof Error ? error.message : 'MCP request failed' }],
|
|
46
|
+
};
|
|
1023
47
|
}
|
|
1024
48
|
});
|
|
1025
49
|
const transport = new StdioServerTransport(process.stdin, process.stdout, {
|
|
1026
|
-
// Include the JSON-lines delimiter in addition to the HTTP envelope budget.
|
|
1027
50
|
maxBufferSize: MAX_MCP_ATTACHMENT_REQUEST_BYTES + 1,
|
|
1028
51
|
});
|
|
1029
|
-
server.connect(transport).catch((
|
|
1030
|
-
console.error('[kobo-tasks-server]
|
|
52
|
+
server.connect(transport).catch(() => {
|
|
53
|
+
console.error('[kobo-tasks-server] Transport failed');
|
|
1031
54
|
process.exit(1);
|
|
1032
55
|
});
|
|
1033
|
-
process.on('SIGTERM', () => {
|
|
1034
|
-
db.close();
|
|
1035
|
-
process.exit(0);
|
|
1036
|
-
});
|
|
1037
|
-
process.on('SIGINT', () => {
|
|
1038
|
-
db.close();
|
|
1039
|
-
process.exit(0);
|
|
1040
|
-
});
|