@klura/mcp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +1 -1
  2. package/index.js +62 -6
  3. package/package.json +2 -2
  4. package/tools.js +9 -1216
package/README.md CHANGED
@@ -46,7 +46,7 @@ Thin wrapper — each MCP `tools/call` dispatches to the corresponding klura run
46
46
  MCP client (Claude Desktop, Cursor, …)
47
47
  │ stdio transport, JSON-RPC
48
48
 
49
- klura-mcp (this package)
49
+ @klura/mcp (this package)
50
50
  │ Node require('@klura/runtime')
51
51
 
52
52
  klura runtime → local daemon → Playwright
package/index.js CHANGED
@@ -36,9 +36,19 @@ async function createKluraMcpServer() {
36
36
  const skillMd = klura.getSkillMd()
37
37
  .replace(/^---[\s\S]*?---\s*/, ''); // strip frontmatter
38
38
 
39
+ // Front-load a terse per-platform capability catalog so agents see what
40
+ // klura already knows BEFORE the first tool call. The list_platform_skills
41
+ // _hint only fires when the agent calls the tool, but the load-bearing
42
+ // failure mode (observed in field) is the agent skipping that call entirely
43
+ // and going straight to start_session for work an existing capability
44
+ // already covers. The deliberate principle break + always-save framing
45
+ // live in the rendered string itself (see getSavedSkillsSummaryMd).
46
+ const savedSkills = klura.getSavedSkillsSummaryMd();
47
+ const instructions = savedSkills ? `${skillMd}\n\n${savedSkills}` : skillMd;
48
+
39
49
  const server = new Server(
40
50
  { name: '@klura/mcp', version: '0.1.0' },
41
- { capabilities: { tools: {}, resources: {} }, instructions: skillMd }
51
+ { capabilities: { tools: {}, resources: {} }, instructions }
42
52
  );
43
53
 
44
54
  // -- Resources (on-demand reference docs) --
@@ -132,7 +142,7 @@ async function createKluraMcpServer() {
132
142
 
133
143
  // -- Tool execution --
134
144
 
135
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
145
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
136
146
  const { name, arguments: rawArgs } = request.params;
137
147
  const tool = toolByName.get(name);
138
148
  if (!tool) {
@@ -143,6 +153,50 @@ async function createKluraMcpServer() {
143
153
  }
144
154
  const args = coerceArgs(name, rawArgs);
145
155
 
156
+ // Progress notifications. When the client request carried
157
+ // `_meta.progressToken`, the SDK exposes it on `extra._meta` and gives us
158
+ // `extra.sendNotification` for sending `notifications/progress` bound to
159
+ // that token. Clients that honor this (Claude Desktop via MCP SDK with
160
+ // `resetTimeoutOnProgress: true`) reset their per-request timeout each
161
+ // time a progress arrives — turning a 4-minute hard deadline into a
162
+ // sliding window that survives long-running tools (end_drive on a real
163
+ // RE session does heavy synthesis + audit + handoff prose composition).
164
+ //
165
+ // Two emit paths:
166
+ // - Explicit phase boundaries inside the tool (e.g. endDrive's
167
+ // progress({stage: '...'}) calls). Names what's running so the user
168
+ // sees specific status, not just "still working".
169
+ // - 30s heartbeat for tools that don't emit explicit progress. Fires
170
+ // only when no explicit progress arrived in the last interval, so
171
+ // instrumented tools don't double-emit.
172
+ let progressCount = 0;
173
+ let lastProgressAt = Date.now();
174
+ let progress;
175
+ let heartbeat;
176
+ const progressToken = extra && extra._meta ? extra._meta.progressToken : undefined;
177
+ if (progressToken !== undefined && extra && typeof extra.sendNotification === 'function') {
178
+ progress = ({ stage, current, total } = {}) => {
179
+ progressCount += 1;
180
+ lastProgressAt = Date.now();
181
+ extra
182
+ .sendNotification({
183
+ method: 'notifications/progress',
184
+ params: {
185
+ progressToken,
186
+ progress: typeof current === 'number' ? current : progressCount,
187
+ ...(typeof total === 'number' ? { total } : {}),
188
+ ...(typeof stage === 'string' ? { message: stage } : {}),
189
+ },
190
+ })
191
+ .catch(() => { /* notification send failure is non-fatal */ });
192
+ };
193
+ heartbeat = setInterval(() => {
194
+ if (Date.now() - lastProgressAt >= 30000) {
195
+ progress({ stage: 'still working' });
196
+ }
197
+ }, 30000);
198
+ }
199
+
146
200
  try {
147
201
  // Phase admissibility — hard tool blocking per the session-phase
148
202
  // state machine. Tools not in the current phase's allowedTools
@@ -203,11 +257,11 @@ async function createKluraMcpServer() {
203
257
  });
204
258
  }
205
259
 
206
- let result = await tool.handler(args);
260
+ let result = await tool.handler(args, { progress });
207
261
 
208
262
  // Inject sticky LIFT obligation reminder. Fires on every tool
209
263
  // response between the first mutating perform_action and either a
210
- // successful save_strategy or close_session ok:true. Once-per-session
264
+ // successful save_strategy or end_drive ok:true. Once-per-session
211
265
  // semantics → no token-binding needed (see runtime/docs/gates.md
212
266
  // §once-vs-many). klura.formatToolResult hoists the obligation
213
267
  // message into a leading [klura obligation]: <message> text block
@@ -239,8 +293,8 @@ async function createKluraMcpServer() {
239
293
  };
240
294
  } catch (err) {
241
295
  // Attach the LIFT obligation to error responses too. Without this,
242
- // every save_strategy / close_session rejection drops the "MUST be
243
- // close_session" anchor exactly when the agent most needs it — agents
296
+ // every save_strategy / end_drive rejection drops the "MUST be
297
+ // end_drive" anchor exactly when the agent most needs it — agents
244
298
  // reading just the bare error treat the failure as a one-off shape
245
299
  // complaint and end the turn after the user-facing goal looks done.
246
300
  let obligationLine = '';
@@ -271,6 +325,8 @@ async function createKluraMcpServer() {
271
325
  content: [{ type: 'text', text: `${obligationLine}Error: ${msg}` }],
272
326
  isError: true,
273
327
  };
328
+ } finally {
329
+ if (heartbeat) clearInterval(heartbeat);
274
330
  }
275
331
  });
276
332
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klura/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@modelcontextprotocol/sdk": "^1.0.0",
34
- "@klura/runtime": "^0.1.0"
34
+ "@klura/runtime": "^0.2.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@eslint/js": "^10.0.1",