@korso/shepherd 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.
- package/README.md +4 -4
- package/dist/index.js +67 -66
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @korso/shepherd — Shepherd MCP Server
|
|
2
2
|
|
|
3
|
-
Shepherd's stdio MCP server. Gives any MCP-capable agent (Claude Code, Codex, etc.)
|
|
3
|
+
Shepherd's stdio MCP server. Gives any MCP-capable agent (Claude Code, Codex, etc.) four advisory coordination tools backed by the shared hub: `work`, `done`, `announce`, and `sync`. The agent **joins the workspace automatically** on startup (no `join` tool), and the server ships standing instructions so the agent self-coordinates without the user prompting it.
|
|
4
4
|
|
|
5
5
|
> **New here?** The [developer quickstart](https://github.com/Korsoai/shepherd/blob/main/docs/shepherd-mcp-quickstart.md) is the fastest path. TL;DR: `npx -y @korso/shepherd` with the env vars below.
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ Shepherd's stdio MCP server. Gives any MCP-capable agent (Claude Code, Codex, et
|
|
|
10
10
|
|
|
11
11
|
> **Everyone must set `WORKSPACE` to the identical string, and that string must equal the hub's `ALLOWED_WORKSPACE` env var.**
|
|
12
12
|
|
|
13
|
-
If `WORKSPACE` does not match, the server's `join` call to the hub returns HTTP 400
|
|
13
|
+
If `WORKSPACE` does not match, the server's automatic `join` call to the hub (fired at startup) returns HTTP 400. Coordination then degrades: every tool reports "session not ready … proceeding uncoordinated" instead of a landscape. This is the most common silent onboarding mistake — if your agent never sees teammates, check `WORKSPACE` first.
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -156,7 +156,7 @@ No stderr output and the process blocking on stdin = healthy. Press Ctrl+C to ex
|
|
|
156
156
|
|
|
157
157
|
and the process exits 1 immediately. This is by design.
|
|
158
158
|
|
|
159
|
-
**Wrong WORKSPACE:** the server starts and connects, but the
|
|
159
|
+
**Wrong WORKSPACE:** the server starts and connects, but the startup auto-join is rejected (400), so every tool call (`work`, `sync`, etc.) reports "session not ready … proceeding uncoordinated". Check that your `WORKSPACE` value exactly matches the hub's `ALLOWED_WORKSPACE`, then restart.
|
|
160
160
|
|
|
161
161
|
---
|
|
162
162
|
|
|
@@ -198,6 +198,6 @@ npm publish --workspace=@korso/shepherd # prepublishOnly runs tsup automatical
|
|
|
198
198
|
| Symptom | Likely cause | Fix |
|
|
199
199
|
|---|---|---|
|
|
200
200
|
| `Configuration error — missing or invalid env vars` | One or more of the 8 env vars is absent | Add the missing vars to your client's `env` block |
|
|
201
|
-
|
|
|
201
|
+
| Tools report "session not ready … proceeding uncoordinated" | Startup auto-join rejected — usually `WORKSPACE` mismatch (or stale `TEAM_TOKEN`) | Set `WORKSPACE` to exactly match the hub's `ALLOWED_WORKSPACE`; re-check `TEAM_TOKEN`; restart |
|
|
202
202
|
| `npm error 404 … @korso/shepherd` | Package not published yet, or name typo | `npm view @korso/shepherd version` to confirm it's live |
|
|
203
203
|
| Process exits immediately with no error | Rare; check for node version incompatibility | Requires Node 18+ (ESM support) |
|
package/dist/index.js
CHANGED
|
@@ -247,71 +247,60 @@ function degradedResult(err) {
|
|
|
247
247
|
function registerTools(server, deps) {
|
|
248
248
|
const { hubClient, config } = deps;
|
|
249
249
|
let sessionId = null;
|
|
250
|
-
let
|
|
250
|
+
let agentName = null;
|
|
251
|
+
const joinBody = {
|
|
252
|
+
workspace: config.WORKSPACE,
|
|
253
|
+
repo: config.REPO,
|
|
254
|
+
branch: config.BRANCH,
|
|
255
|
+
human: config.HUMAN,
|
|
256
|
+
program: config.PROGRAM,
|
|
257
|
+
model: config.MODEL
|
|
258
|
+
};
|
|
259
|
+
const joinInFlight = hubClient.post("/join", joinBody).then((r) => {
|
|
260
|
+
sessionId = r.sessionId;
|
|
261
|
+
agentName = r.agentName;
|
|
262
|
+
}).catch(() => {
|
|
263
|
+
});
|
|
251
264
|
async function awaitJoin() {
|
|
252
|
-
|
|
265
|
+
await joinInFlight;
|
|
253
266
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
},
|
|
261
|
-
async (_args) => {
|
|
262
|
-
try {
|
|
263
|
-
const body = {
|
|
264
|
-
workspace: config.WORKSPACE,
|
|
265
|
-
repo: config.REPO,
|
|
266
|
-
branch: config.BRANCH,
|
|
267
|
-
human: config.HUMAN,
|
|
268
|
-
program: config.PROGRAM,
|
|
269
|
-
model: config.MODEL
|
|
270
|
-
};
|
|
271
|
-
const pending = hubClient.post("/join", body);
|
|
272
|
-
joinInFlight = pending.then((r) => {
|
|
273
|
-
sessionId = r.sessionId;
|
|
274
|
-
}).catch(() => {
|
|
275
|
-
});
|
|
276
|
-
const result = await pending;
|
|
277
|
-
sessionId = result.sessionId;
|
|
278
|
-
return {
|
|
279
|
-
content: [
|
|
280
|
-
{
|
|
281
|
-
type: "text",
|
|
282
|
-
text: `Joined as ${result.agentName}.`
|
|
283
|
-
}
|
|
284
|
-
]
|
|
285
|
-
};
|
|
286
|
-
} catch (err) {
|
|
287
|
-
if (err instanceof HubUnreachable || err instanceof HubRequestError) {
|
|
288
|
-
return degradedResult(err);
|
|
267
|
+
function sessionNotReady() {
|
|
268
|
+
return {
|
|
269
|
+
content: [
|
|
270
|
+
{
|
|
271
|
+
type: "text",
|
|
272
|
+
text: "Shepherd coordination session not ready (hub unreachable at startup) \u2014 proceeding uncoordinated."
|
|
289
273
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
)
|
|
274
|
+
]
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function withIdentity(body) {
|
|
278
|
+
return agentName ? `You are ${agentName}.
|
|
279
|
+
|
|
280
|
+
${body}` : body;
|
|
281
|
+
}
|
|
294
282
|
server.registerTool(
|
|
295
283
|
"work",
|
|
296
284
|
{
|
|
297
285
|
title: "Claim a unit of work",
|
|
298
|
-
description:
|
|
286
|
+
description: 'Claim a unit of work BEFORE you start editing files in an area of the codebase (per unit of work, NOT per edit). Pass a one-line `intent` and the `pathGlobs` covering the files you expect to touch \u2014 scope them as specifically as you reasonably can (e.g. ["src/auth/**"], not ["src/**"] and not a single file). It atomically checks whether a teammate is already in those files and claims them for you, returning any conflicts and what others are working on. Hold one claim across all edits in that area; don\'t re-claim per file.',
|
|
299
287
|
inputSchema: WorkAgentInput.shape
|
|
300
288
|
},
|
|
301
289
|
async (args) => {
|
|
302
290
|
await awaitJoin();
|
|
303
291
|
if (sessionId === null) {
|
|
304
|
-
return
|
|
305
|
-
isError: true,
|
|
306
|
-
content: [{ type: "text", text: "Call join first \u2014 no active session." }]
|
|
307
|
-
};
|
|
292
|
+
return sessionNotReady();
|
|
308
293
|
}
|
|
309
294
|
try {
|
|
310
295
|
const body = { sessionId, ...args };
|
|
311
296
|
const result = await hubClient.post("/work", body);
|
|
312
|
-
const text =
|
|
297
|
+
const text = withIdentity(
|
|
298
|
+
`Work claimed (workItemId: ${result.workItemId})
|
|
299
|
+
|
|
300
|
+
` + formatLandscape(result.landscape) + `
|
|
313
301
|
|
|
314
|
-
|
|
302
|
+
You hold this claim until you call done (workItemId: ${result.workItemId}) or it expires (~30 min). Calling work or sync renews it.`
|
|
303
|
+
);
|
|
315
304
|
return { content: [{ type: "text", text }] };
|
|
316
305
|
} catch (err) {
|
|
317
306
|
if (err instanceof HubUnreachable || err instanceof HubRequestError) {
|
|
@@ -325,16 +314,13 @@ function registerTools(server, deps) {
|
|
|
325
314
|
"done",
|
|
326
315
|
{
|
|
327
316
|
title: "Release a work claim",
|
|
328
|
-
description: "Call when
|
|
317
|
+
description: "Call when a unit of work is complete to release your claim so teammates know the files are free. Pass the workItemId returned by the work tool.",
|
|
329
318
|
inputSchema: DoneAgentInput.shape
|
|
330
319
|
},
|
|
331
320
|
async (args) => {
|
|
332
321
|
await awaitJoin();
|
|
333
322
|
if (sessionId === null) {
|
|
334
|
-
return
|
|
335
|
-
isError: true,
|
|
336
|
-
content: [{ type: "text", text: "Call join first \u2014 no active session." }]
|
|
337
|
-
};
|
|
323
|
+
return sessionNotReady();
|
|
338
324
|
}
|
|
339
325
|
try {
|
|
340
326
|
const body = { sessionId, ...args };
|
|
@@ -343,7 +329,7 @@ function registerTools(server, deps) {
|
|
|
343
329
|
content: [
|
|
344
330
|
{
|
|
345
331
|
type: "text",
|
|
346
|
-
text: "Work item released."
|
|
332
|
+
text: "Work item released. Call work again before your next edit in a new area."
|
|
347
333
|
}
|
|
348
334
|
]
|
|
349
335
|
};
|
|
@@ -359,16 +345,13 @@ function registerTools(server, deps) {
|
|
|
359
345
|
"announce",
|
|
360
346
|
{
|
|
361
347
|
title: "Broadcast a message to teammates",
|
|
362
|
-
description: "Broadcast a heads-up to the other agents
|
|
348
|
+
description: "Broadcast a heads-up to the other agents, or direct a finding to a specific agent. This is awareness only \u2014 not a task assignment. To direct it, pass that agent's name (exactly as shown in the landscape) as targetAgentName; omit it to broadcast to everyone in the workspace. Delivery is best-effort: the recipient sees it on their next work/sync, once.",
|
|
363
349
|
inputSchema: AnnounceAgentInput.shape
|
|
364
350
|
},
|
|
365
351
|
async (args) => {
|
|
366
352
|
await awaitJoin();
|
|
367
353
|
if (sessionId === null) {
|
|
368
|
-
return
|
|
369
|
-
isError: true,
|
|
370
|
-
content: [{ type: "text", text: "Call join first \u2014 no active session." }]
|
|
371
|
-
};
|
|
354
|
+
return sessionNotReady();
|
|
372
355
|
}
|
|
373
356
|
try {
|
|
374
357
|
const body = { sessionId, ...args };
|
|
@@ -393,21 +376,18 @@ function registerTools(server, deps) {
|
|
|
393
376
|
"sync",
|
|
394
377
|
{
|
|
395
378
|
title: "Sync team landscape",
|
|
396
|
-
description: "
|
|
379
|
+
description: "Pull the latest team landscape (who's working on what, any messages for you) and renew your active claims. Call when you resume, start a new task, or before large changes \u2014 or any time you want to check for teammate activity without claiming work.",
|
|
397
380
|
inputSchema: SyncAgentInput.shape
|
|
398
381
|
},
|
|
399
382
|
async (_args) => {
|
|
400
383
|
await awaitJoin();
|
|
401
384
|
if (sessionId === null) {
|
|
402
|
-
return
|
|
403
|
-
isError: true,
|
|
404
|
-
content: [{ type: "text", text: "Call join first \u2014 no active session." }]
|
|
405
|
-
};
|
|
385
|
+
return sessionNotReady();
|
|
406
386
|
}
|
|
407
387
|
try {
|
|
408
388
|
const body = { sessionId };
|
|
409
389
|
const result = await hubClient.post("/sync", body);
|
|
410
|
-
const text = formatLandscape(result.landscape);
|
|
390
|
+
const text = withIdentity(formatLandscape(result.landscape));
|
|
411
391
|
return { content: [{ type: "text", text }] };
|
|
412
392
|
} catch (err) {
|
|
413
393
|
if (err instanceof HubUnreachable || err instanceof HubRequestError) {
|
|
@@ -417,13 +397,34 @@ function registerTools(server, deps) {
|
|
|
417
397
|
}
|
|
418
398
|
}
|
|
419
399
|
);
|
|
400
|
+
return { ready: joinInFlight };
|
|
420
401
|
}
|
|
421
402
|
|
|
403
|
+
// src/instructions.ts
|
|
404
|
+
var SHEPHERD_INSTRUCTIONS = `You are connected to Shepherd, the shared coordination hub for a team of agents (human and AI) working in the same repositories. You have ALREADY been joined to the workspace automatically \u2014 there is no setup step, and you must not ask the user how to connect.
|
|
405
|
+
|
|
406
|
+
Follow this procedure on every session, proactively and without being asked:
|
|
407
|
+
|
|
408
|
+
1. Before you start working on an AREA of the codebase, call \`work\` ONCE. Pass a one-line \`intent\` and the \`pathGlobs\` covering the files you expect to touch. Scope the globs as specifically as you reasonably can \u2014 tight enough to avoid colliding with unrelated work, broad enough to cover the task (e.g. ["src/auth/**"], not ["src/**"] and not a single file). Hold that one claim across all your edits in that area; do NOT re-claim per file. If it reports a conflict, coordinate or pick different work \u2014 never silently collide.
|
|
409
|
+
|
|
410
|
+
2. Call \`done\` when that unit of work is complete, using its \`workItemId\`, so teammates see the files freed.
|
|
411
|
+
|
|
412
|
+
3. Re-call \`work\` only when you move to a DIFFERENT area not covered by a live claim. (\`work\` and \`sync\` also renew your existing claims.)
|
|
413
|
+
|
|
414
|
+
4. Call \`announce\` whenever you discover something another agent needs \u2014 a shared decision, a gotcha, an API change, a finding. If the landscape shows a specific agent working in the affected area, direct it to them by passing their name as \`targetAgentName\`; otherwise broadcast. Awareness only, not task assignment.
|
|
415
|
+
|
|
416
|
+
5. Call \`sync\` when you resume, start a new task, or before large changes, to refresh who is doing what.
|
|
417
|
+
|
|
418
|
+
Skip \`work\` entirely for read-only exploration. These tools are advisory and degrade gracefully if the hub is unreachable \u2014 never block your real work on them.`;
|
|
419
|
+
|
|
422
420
|
// src/index.ts
|
|
423
421
|
async function main() {
|
|
424
422
|
const config = loadConfig();
|
|
425
423
|
const hubClient = createHubClient({ hubUrl: config.HUB_URL, teamToken: config.TEAM_TOKEN });
|
|
426
|
-
const server = new McpServer(
|
|
424
|
+
const server = new McpServer(
|
|
425
|
+
{ name: "shepherd", version: "0.1.0" },
|
|
426
|
+
{ instructions: SHEPHERD_INSTRUCTIONS }
|
|
427
|
+
);
|
|
427
428
|
registerTools(server, { hubClient, config });
|
|
428
429
|
const transport = new StdioServerTransport();
|
|
429
430
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@korso/shepherd",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shepherd MCP server — gives any MCP-capable agent (Claude Code, Codex, etc.)
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Shepherd MCP server — gives any MCP-capable agent (Claude Code, Codex, etc.) four advisory cross-session coordination tools (work/done/announce/sync) backed by the shared Shepherd hub. Joins the workspace automatically and ships standing instructions so the agent self-coordinates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|