@korso/shepherd 0.1.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 (3) hide show
  1. package/README.md +203 -0
  2. package/dist/index.js +438 -0
  3. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # @korso/shepherd — Shepherd MCP Server
2
+
3
+ Shepherd's stdio MCP server. Gives any MCP-capable agent (Claude Code, Codex, etc.) five advisory coordination tools backed by the shared hub: `join`, `work`, `done`, `announce`, and `sync`.
4
+
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
+
7
+ ---
8
+
9
+ ## CRITICAL: WORKSPACE must match the hub exactly
10
+
11
+ > **Everyone must set `WORKSPACE` to the identical string, and that string must equal the hub's `ALLOWED_WORKSPACE` env var.**
12
+
13
+ If `WORKSPACE` does not match, the server's `join` call to the hub returns HTTP 400 immediately. The error is loud on purpose — you will see it in stderr the moment the first tool is called. This is the most common silent onboarding mistake; the guard turns it into a loud, immediate failure.
14
+
15
+ ---
16
+
17
+ ## Install
18
+
19
+ The server is published to npm and runs via `npx` — no clone or build required
20
+ (Node 18+):
21
+
22
+ ```sh
23
+ npx -y @korso/shepherd
24
+ ```
25
+
26
+ You won't normally run that by hand; you put it in your MCP client config (below)
27
+ with the required env vars. `npx` caches the package, so startup is fast after the
28
+ first fetch, and `@korso/shepherd@latest` picks up updates automatically.
29
+
30
+ > Hacking on the server itself? See **[Develop from source](#develop-from-source)**
31
+ > at the bottom.
32
+
33
+ ---
34
+
35
+ ## 2. Required environment variables
36
+
37
+ Every instance of the MCP server needs these eight env vars set in its client config:
38
+
39
+ | Variable | Description | Example |
40
+ |---|---|---|
41
+ | `HUB_URL` | Base URL of the deployed hub | `https://shepherd.example.com` |
42
+ | `TEAM_TOKEN` | Shared bearer token accepted by the hub | `tok_abc123` |
43
+ | `WORKSPACE` | **Must match hub's `ALLOWED_WORKSPACE` exactly** | `shepherd` |
44
+ | `REPO` | Repository slug (used for scoping claims) | `shepherd` |
45
+ | `BRANCH` | Git branch name | `main` |
46
+ | `HUMAN` | Founder name — identifies you in the presence feed | `daichi` |
47
+ | `PROGRAM` | Agent program name | `claude-code` |
48
+ | `MODEL` | Model ID being used | `claude-sonnet-4-6` |
49
+
50
+ All eight are required. Missing any one causes an immediate startup failure with a clear error on stderr listing which vars are absent.
51
+
52
+ ---
53
+
54
+ ## 3. MCP client configuration
55
+
56
+ ### Claude Code
57
+
58
+ > **Do not use `~/.claude/mcp.json` — Claude Code does not read it** (a config
59
+ > there loads silently into nothing). Use `claude mcp add` (user scope, applies
60
+ > everywhere) or a project-root `.mcp.json`. Confirm with `claude mcp list`,
61
+ > which should show `shepherd … ✔ Connected`.
62
+
63
+ Recommended — register once at user scope (works identically on Windows/macOS/Linux):
64
+
65
+ ```sh
66
+ claude mcp add shepherd -s user \
67
+ -e HUB_URL=https://shepherd.example.com \
68
+ -e TEAM_TOKEN=tok_abc123 \
69
+ -e WORKSPACE=shepherd \
70
+ -e REPO=shepherd -e BRANCH=main -e HUMAN=daichi \
71
+ -e PROGRAM=claude-code -e MODEL=claude-sonnet-4-6 \
72
+ -- npx -y @korso/shepherd
73
+ ```
74
+
75
+ Alternative — a `.mcp.json` at the **root of the repo you're working in**:
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "shepherd": {
81
+ "command": "npx",
82
+ "args": ["-y", "@korso/shepherd"],
83
+ "env": {
84
+ "HUB_URL": "https://shepherd.example.com",
85
+ "TEAM_TOKEN": "tok_abc123",
86
+ "WORKSPACE": "shepherd",
87
+ "REPO": "shepherd",
88
+ "BRANCH": "main",
89
+ "HUMAN": "daichi",
90
+ "PROGRAM": "claude-code",
91
+ "MODEL": "claude-sonnet-4-6"
92
+ }
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ > Windows note: the server is a thin stdio client to the Linux-hosted hub, and
99
+ > `npx` works the same on every OS — no file paths to escape. The hub itself runs
100
+ > on Linux (Postgres), so the Windows-native durability concerns from the spike
101
+ > don't apply to clients.
102
+
103
+ ### Codex (`~/.codex/config.json` or `codex.json`)
104
+
105
+ Codex uses the same MCP stdio protocol. Add a server entry under `mcpServers`:
106
+
107
+ ```json
108
+ {
109
+ "mcpServers": {
110
+ "shepherd": {
111
+ "command": "npx",
112
+ "args": ["-y", "@korso/shepherd"],
113
+ "env": {
114
+ "HUB_URL": "https://shepherd.example.com",
115
+ "TEAM_TOKEN": "tok_abc123",
116
+ "WORKSPACE": "shepherd",
117
+ "REPO": "shepherd",
118
+ "BRANCH": "main",
119
+ "HUMAN": "alex",
120
+ "PROGRAM": "codex",
121
+ "MODEL": "o4-mini"
122
+ }
123
+ }
124
+ }
125
+ }
126
+ ```
127
+
128
+ ---
129
+
130
+ ## 4. Verify the server starts (quick smoke test)
131
+
132
+ Run with all env vars set to confirm it connects and idles on stdin:
133
+
134
+ ```sh
135
+ HUB_URL=https://shepherd.example.com \
136
+ TEAM_TOKEN=tok_abc123 \
137
+ WORKSPACE=shepherd \
138
+ REPO=shepherd \
139
+ BRANCH=main \
140
+ HUMAN=daichi \
141
+ PROGRAM=claude-code \
142
+ MODEL=claude-sonnet-4-6 \
143
+ npx -y @korso/shepherd
144
+ ```
145
+
146
+ No stderr output and the process blocking on stdin = healthy. Press Ctrl+C to exit.
147
+
148
+ **Missing env vars:** if you deliberately omit a var, you will see:
149
+
150
+ ```
151
+ [shepherd] Configuration error — missing or invalid env vars:
152
+ HUB_URL: HUB_URL is required
153
+ TEAM_TOKEN: TEAM_TOKEN is required
154
+ ...
155
+ ```
156
+
157
+ and the process exits 1 immediately. This is by design.
158
+
159
+ **Wrong WORKSPACE:** the server starts and connects, but the first tool call (`work`, `sync`, etc.) returns a 400 from the hub. Check that your `WORKSPACE` value exactly matches the hub's `ALLOWED_WORKSPACE`.
160
+
161
+ ---
162
+
163
+ ## Develop from source
164
+
165
+ Only needed if you're changing the MCP server itself. Clone the monorepo and
166
+ point your client at a local build instead of npx:
167
+
168
+ ```sh
169
+ git clone https://github.com/Korsoai/shepherd.git
170
+ cd shepherd
171
+ npm install
172
+ npm run build # tsc -b — compiles the workspace for dev + tests
173
+ ```
174
+
175
+ For an exact preview of the published artifact (a single self-contained bundle
176
+ with `@shepherd/shared` inlined), build the package directly:
177
+
178
+ ```sh
179
+ npm run build --workspace=@korso/shepherd # runs tsup → packages/mcp-server/dist/index.js
180
+ ```
181
+
182
+ Then use `node /absolute/path/to/shepherd/packages/mcp-server/dist/index.js` as
183
+ the `command` in your MCP config (Windows: escape backslashes in JSON).
184
+
185
+ ### Publishing a new version
186
+
187
+ ```sh
188
+ # bump "version" in packages/mcp-server/package.json, then:
189
+ npm publish --workspace=@korso/shepherd # prepublishOnly runs tsup automatically
190
+ ```
191
+
192
+ `publishConfig.access` is `public`, so the scoped package publishes publicly.
193
+
194
+ ---
195
+
196
+ ## Troubleshooting
197
+
198
+ | Symptom | Likely cause | Fix |
199
+ |---|---|---|
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
+ | Hub returns 400 on first tool call | `WORKSPACE` mismatch between client and hub | Set `WORKSPACE` to exactly match the hub's `ALLOWED_WORKSPACE` |
202
+ | `npm error 404 … @korso/shepherd` | Package not published yet, or name typo | `npm view @korso/shepherd version` to confirm it's live |
203
+ | Process exits immediately with no error | Rare; check for node version incompatibility | Requires Node 18+ (ESM support) |
package/dist/index.js ADDED
@@ -0,0 +1,438 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/config.ts
8
+ import { z } from "zod";
9
+ var ConfigSchema = z.object({
10
+ HUB_URL: z.string().min(1, "HUB_URL is required"),
11
+ TEAM_TOKEN: z.string().min(1, "TEAM_TOKEN is required"),
12
+ WORKSPACE: z.string().min(1, "WORKSPACE is required"),
13
+ REPO: z.string().min(1, "REPO is required"),
14
+ BRANCH: z.string().min(1, "BRANCH is required"),
15
+ HUMAN: z.string().min(1, "HUMAN is required"),
16
+ PROGRAM: z.string().min(1, "PROGRAM is required"),
17
+ MODEL: z.string().min(1, "MODEL is required")
18
+ });
19
+ function parseConfig(env) {
20
+ return ConfigSchema.parse({
21
+ HUB_URL: env["HUB_URL"],
22
+ TEAM_TOKEN: env["TEAM_TOKEN"],
23
+ WORKSPACE: env["WORKSPACE"],
24
+ REPO: env["REPO"],
25
+ BRANCH: env["BRANCH"],
26
+ HUMAN: env["HUMAN"],
27
+ PROGRAM: env["PROGRAM"],
28
+ MODEL: env["MODEL"]
29
+ });
30
+ }
31
+ function loadConfig(env = process.env) {
32
+ try {
33
+ return parseConfig(env);
34
+ } catch (err) {
35
+ if (err instanceof z.ZodError) {
36
+ const messages = err.issues.map((e) => ` ${e.path.join(".")}: ${e.message}`).join("\n");
37
+ process.stderr.write(`[shepherd] Configuration error \u2014 missing or invalid env vars:
38
+ ${messages}
39
+ `);
40
+ } else {
41
+ process.stderr.write(`[shepherd] Unexpected configuration error: ${String(err)}
42
+ `);
43
+ }
44
+ process.exit(1);
45
+ }
46
+ }
47
+
48
+ // src/hubClient.ts
49
+ var DEFAULT_TIMEOUT_MS = 5e3;
50
+ var HubUnreachable = class extends Error {
51
+ constructor(message, cause) {
52
+ super(message);
53
+ this.name = "HubUnreachable";
54
+ if (cause !== void 0) {
55
+ this.cause = cause;
56
+ }
57
+ }
58
+ };
59
+ var HubRequestError = class extends Error {
60
+ status;
61
+ constructor(status, message) {
62
+ super(message);
63
+ this.name = "HubRequestError";
64
+ this.status = status;
65
+ }
66
+ };
67
+ function createHubClient({
68
+ hubUrl,
69
+ teamToken,
70
+ timeoutMs = DEFAULT_TIMEOUT_MS
71
+ }) {
72
+ const baseUrl = hubUrl.replace(/\/$/, "");
73
+ return {
74
+ async post(path, body) {
75
+ const controller = new AbortController();
76
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
77
+ let response;
78
+ try {
79
+ response = await fetch(`${baseUrl}${path}`, {
80
+ method: "POST",
81
+ headers: {
82
+ "Authorization": `Bearer ${teamToken}`,
83
+ "Content-Type": "application/json"
84
+ },
85
+ body: JSON.stringify(body),
86
+ signal: controller.signal
87
+ });
88
+ } catch (err) {
89
+ clearTimeout(timer);
90
+ const message = err instanceof DOMException && err.name === "AbortError" ? `Hub request timed out after ${timeoutMs}ms (${path})` : `Hub unreachable at ${baseUrl}${path}: ${String(err)}`;
91
+ throw new HubUnreachable(message, err);
92
+ } finally {
93
+ clearTimeout(timer);
94
+ }
95
+ if (!response.ok) {
96
+ throw new HubRequestError(
97
+ response.status,
98
+ `Hub returned HTTP ${response.status} for ${path}`
99
+ );
100
+ }
101
+ return response.json();
102
+ }
103
+ };
104
+ }
105
+
106
+ // ../shared/dist/contract.js
107
+ import { z as z2 } from "zod";
108
+ var IsoTimestamp = z2.string();
109
+ var DbId = z2.number();
110
+ var Claim = z2.object({
111
+ workItemId: z2.string().uuid(),
112
+ agentName: z2.string(),
113
+ human: z2.string().min(1),
114
+ intent: z2.string().min(1).max(2048),
115
+ pathGlobs: z2.array(z2.string().min(1).max(512)).min(1).max(64),
116
+ // ISO timestamp string; see IsoTimestamp note above
117
+ expiresAt: IsoTimestamp
118
+ });
119
+ var Announcement = z2.object({
120
+ // bigint PK serialised as number; see DbId note above
121
+ id: DbId,
122
+ fromAgentName: z2.string(),
123
+ fromHuman: z2.string().min(1),
124
+ body: z2.string().min(1).max(8192),
125
+ targetAgentName: z2.string().nullable(),
126
+ // ISO timestamp string; see IsoTimestamp note above
127
+ createdAt: IsoTimestamp
128
+ });
129
+ var Landscape = z2.object({
130
+ conflicts: z2.array(Claim),
131
+ activeClaims: z2.array(Claim),
132
+ // The caller's OWN active claims. `activeClaims` deliberately excludes the
133
+ // caller's session, so without this an agent has no way to confirm its own
134
+ // claim is live. Optional with a default so an older client talking to a
135
+ // newer hub (or vice-versa) never fails validation on its absence.
136
+ yourClaims: z2.array(Claim).default([]),
137
+ announcements: z2.array(Announcement)
138
+ });
139
+ var JoinRequest = z2.object({
140
+ workspace: z2.string().min(1),
141
+ repo: z2.string().min(1),
142
+ branch: z2.string().min(1),
143
+ human: z2.string().min(1),
144
+ program: z2.string().min(1),
145
+ model: z2.string().min(1)
146
+ });
147
+ var JoinResponse = z2.object({
148
+ agentName: z2.string(),
149
+ sessionId: z2.string().uuid()
150
+ });
151
+ var WorkRequest = z2.object({
152
+ sessionId: z2.string().uuid(),
153
+ intent: z2.string().min(1).max(2048),
154
+ pathGlobs: z2.array(z2.string().min(1).max(512)).min(1).max(64),
155
+ ttlSeconds: z2.number().int().positive().optional()
156
+ });
157
+ var WorkResponse = z2.object({
158
+ workItemId: z2.string().uuid(),
159
+ landscape: Landscape
160
+ });
161
+ var DoneRequest = z2.object({
162
+ sessionId: z2.string().uuid(),
163
+ workItemId: z2.string().uuid()
164
+ });
165
+ var DoneResponse = z2.object({
166
+ ok: z2.literal(true)
167
+ });
168
+ var AnnounceRequest = z2.object({
169
+ sessionId: z2.string().uuid(),
170
+ body: z2.string().min(1).max(8192),
171
+ // absent or null => broadcast to all agents in the workspace
172
+ targetAgentName: z2.string().nullable().optional()
173
+ });
174
+ var AnnounceResponse = z2.object({
175
+ ok: z2.literal(true),
176
+ // bigint PK serialised as number; see DbId note above
177
+ announcementId: DbId
178
+ });
179
+ var SyncRequest = z2.object({
180
+ sessionId: z2.string().uuid()
181
+ });
182
+ var SyncResponse = z2.object({
183
+ landscape: Landscape
184
+ });
185
+ var WorkAgentInput = WorkRequest.omit({ sessionId: true });
186
+ var AnnounceAgentInput = AnnounceRequest.omit({ sessionId: true });
187
+ var DoneAgentInput = DoneRequest.omit({ sessionId: true });
188
+ var JoinAgentInput = z2.object({});
189
+ var SyncAgentInput = z2.object({});
190
+
191
+ // src/tools.ts
192
+ function formatLandscape(landscape) {
193
+ const lines = [];
194
+ if (landscape.conflicts.length > 0) {
195
+ lines.push("CONFLICTS (files overlapping with your claim):");
196
+ for (const c of landscape.conflicts) {
197
+ lines.push(
198
+ ` [${c.agentName} / ${c.human}] "${c.intent}" \u2014 globs: ${c.pathGlobs.join(", ")}`
199
+ );
200
+ }
201
+ } else {
202
+ lines.push("CONFLICTS: none");
203
+ }
204
+ if (landscape.activeClaims.length > 0) {
205
+ lines.push("ACTIVE CLAIMS (other agents currently working):");
206
+ for (const c of landscape.activeClaims) {
207
+ lines.push(
208
+ ` [${c.agentName} / ${c.human}] "${c.intent}" \u2014 globs: ${c.pathGlobs.join(", ")}`
209
+ );
210
+ }
211
+ } else {
212
+ lines.push("ACTIVE CLAIMS: none");
213
+ }
214
+ const yourClaims = landscape.yourClaims ?? [];
215
+ if (yourClaims.length > 0) {
216
+ lines.push("YOUR ACTIVE CLAIMS:");
217
+ for (const c of yourClaims) {
218
+ lines.push(
219
+ ` "${c.intent}" \u2014 globs: ${c.pathGlobs.join(", ")} (workItemId: ${c.workItemId})`
220
+ );
221
+ }
222
+ } else {
223
+ lines.push("YOUR ACTIVE CLAIMS: none");
224
+ }
225
+ if (landscape.announcements.length > 0) {
226
+ lines.push("ANNOUNCEMENTS:");
227
+ for (const a of landscape.announcements) {
228
+ const target = a.targetAgentName ? ` \u2192 ${a.targetAgentName}` : " (broadcast)";
229
+ lines.push(` [${a.fromAgentName}${target}] ${a.body}`);
230
+ }
231
+ } else {
232
+ lines.push("ANNOUNCEMENTS: none");
233
+ }
234
+ return lines.join("\n");
235
+ }
236
+ function degradedResult(err) {
237
+ const detail = err instanceof HubUnreachable || err instanceof HubRequestError ? err.message : String(err);
238
+ return {
239
+ content: [
240
+ {
241
+ type: "text",
242
+ text: `Coordination hub unreachable \u2014 proceeding uncoordinated. ${detail}`
243
+ }
244
+ ]
245
+ };
246
+ }
247
+ function registerTools(server, deps) {
248
+ const { hubClient, config } = deps;
249
+ let sessionId = null;
250
+ let joinInFlight = null;
251
+ async function awaitJoin() {
252
+ if (joinInFlight) await joinInFlight;
253
+ }
254
+ server.registerTool(
255
+ "join",
256
+ {
257
+ title: "Join coordination hub",
258
+ description: "Register yourself with the team coordination hub at the start of a session. Call once before any other coordination tool. Returns your assigned agent name and session ID that the other tools use automatically.",
259
+ inputSchema: JoinAgentInput.shape
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);
289
+ }
290
+ throw err;
291
+ }
292
+ }
293
+ );
294
+ server.registerTool(
295
+ "work",
296
+ {
297
+ title: "Claim a unit of work",
298
+ description: "Call this BEFORE you start a unit of work. It atomically checks whether any teammate's agent is already touching the same files and claims the work for you, returning any conflicts and what others are working on. Always call this before editing files.",
299
+ inputSchema: WorkAgentInput.shape
300
+ },
301
+ async (args) => {
302
+ await awaitJoin();
303
+ if (sessionId === null) {
304
+ return {
305
+ isError: true,
306
+ content: [{ type: "text", text: "Call join first \u2014 no active session." }]
307
+ };
308
+ }
309
+ try {
310
+ const body = { sessionId, ...args };
311
+ const result = await hubClient.post("/work", body);
312
+ const text = `Work claimed (workItemId: ${result.workItemId})
313
+
314
+ ` + formatLandscape(result.landscape);
315
+ return { content: [{ type: "text", text }] };
316
+ } catch (err) {
317
+ if (err instanceof HubUnreachable || err instanceof HubRequestError) {
318
+ return degradedResult(err);
319
+ }
320
+ throw err;
321
+ }
322
+ }
323
+ );
324
+ server.registerTool(
325
+ "done",
326
+ {
327
+ title: "Release a work claim",
328
+ description: "Call when you finish a unit of work to release your claim so teammates know the files are free. Pass the workItemId returned by the work tool.",
329
+ inputSchema: DoneAgentInput.shape
330
+ },
331
+ async (args) => {
332
+ await awaitJoin();
333
+ if (sessionId === null) {
334
+ return {
335
+ isError: true,
336
+ content: [{ type: "text", text: "Call join first \u2014 no active session." }]
337
+ };
338
+ }
339
+ try {
340
+ const body = { sessionId, ...args };
341
+ await hubClient.post("/done", body);
342
+ return {
343
+ content: [
344
+ {
345
+ type: "text",
346
+ text: "Work item released."
347
+ }
348
+ ]
349
+ };
350
+ } catch (err) {
351
+ if (err instanceof HubUnreachable || err instanceof HubRequestError) {
352
+ return degradedResult(err);
353
+ }
354
+ throw err;
355
+ }
356
+ }
357
+ );
358
+ server.registerTool(
359
+ "announce",
360
+ {
361
+ title: "Broadcast a message to teammates",
362
+ description: "Broadcast a heads-up to the other agents (or hand a specific agent a finding). This is awareness only \u2014 not a task assignment. Omit targetAgentName to broadcast to everyone in the workspace.",
363
+ inputSchema: AnnounceAgentInput.shape
364
+ },
365
+ async (args) => {
366
+ await awaitJoin();
367
+ if (sessionId === null) {
368
+ return {
369
+ isError: true,
370
+ content: [{ type: "text", text: "Call join first \u2014 no active session." }]
371
+ };
372
+ }
373
+ try {
374
+ const body = { sessionId, ...args };
375
+ const result = await hubClient.post("/announce", body);
376
+ return {
377
+ content: [
378
+ {
379
+ type: "text",
380
+ text: `Announcement sent (id: ${result.announcementId}).`
381
+ }
382
+ ]
383
+ };
384
+ } catch (err) {
385
+ if (err instanceof HubUnreachable || err instanceof HubRequestError) {
386
+ return degradedResult(err);
387
+ }
388
+ throw err;
389
+ }
390
+ }
391
+ );
392
+ server.registerTool(
393
+ "sync",
394
+ {
395
+ title: "Sync team landscape",
396
+ description: "Optional periodic pull of the latest team landscape (who's working on what, any messages for you). Also keeps your claims alive. Call this if you want to check for teammate activity without starting new work.",
397
+ inputSchema: SyncAgentInput.shape
398
+ },
399
+ async (_args) => {
400
+ await awaitJoin();
401
+ if (sessionId === null) {
402
+ return {
403
+ isError: true,
404
+ content: [{ type: "text", text: "Call join first \u2014 no active session." }]
405
+ };
406
+ }
407
+ try {
408
+ const body = { sessionId };
409
+ const result = await hubClient.post("/sync", body);
410
+ const text = formatLandscape(result.landscape);
411
+ return { content: [{ type: "text", text }] };
412
+ } catch (err) {
413
+ if (err instanceof HubUnreachable || err instanceof HubRequestError) {
414
+ return degradedResult(err);
415
+ }
416
+ throw err;
417
+ }
418
+ }
419
+ );
420
+ }
421
+
422
+ // src/index.ts
423
+ async function main() {
424
+ const config = loadConfig();
425
+ const hubClient = createHubClient({ hubUrl: config.HUB_URL, teamToken: config.TEAM_TOKEN });
426
+ const server = new McpServer({ name: "shepherd", version: "0.1.0" });
427
+ registerTools(server, { hubClient, config });
428
+ const transport = new StdioServerTransport();
429
+ await server.connect(transport);
430
+ }
431
+ main().catch((err) => {
432
+ process.stderr.write(`[shepherd] Fatal boot error: ${String(err)}
433
+ `);
434
+ if (err instanceof Error && err.stack) {
435
+ process.stderr.write(err.stack + "\n");
436
+ }
437
+ process.exit(1);
438
+ });
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@korso/shepherd",
3
+ "version": "0.1.0",
4
+ "description": "Shepherd MCP server — gives any MCP-capable agent (Claude Code, Codex, etc.) five advisory cross-session coordination tools (join/work/done/announce/sync) backed by the shared Shepherd hub.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "shepherd-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "license": "UNLICENSED",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Korsoai/shepherd.git",
21
+ "directory": "packages/mcp-server"
22
+ },
23
+ "keywords": [
24
+ "mcp",
25
+ "model-context-protocol",
26
+ "claude",
27
+ "agent",
28
+ "coordination"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "tsup",
35
+ "start": "node dist/index.js",
36
+ "prepublishOnly": "tsup"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.29",
40
+ "zod": "^3"
41
+ },
42
+ "devDependencies": {
43
+ "@shepherd/shared": "*",
44
+ "tsup": "^8"
45
+ }
46
+ }