@kynver-app/mcp-agent-os 0.3.5 → 0.3.7

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 CHANGED
@@ -51,6 +51,19 @@ AgentTask board (Phase 2 — batch 1 CRUD + batch 1.5 steering/lease):
51
51
  - `agent_os_task_claim`
52
52
  - `agent_os_task_renew_lease`
53
53
 
54
+ AgentPlan first-class plans (Phase 5A — versioned operational artifacts
55
+ separate from goals/projects/tasks):
56
+
57
+ - `agent_os_plan_create`
58
+ - `agent_os_plan_list`
59
+ - `agent_os_plan_get`
60
+ - `agent_os_plan_update`
61
+ - `agent_os_plan_add_version`
62
+ - `agent_os_plan_mark_current`
63
+ - `agent_os_plan_add_link`
64
+ - `agent_os_plan_list_links`
65
+ - `agent_os_plan_list_versions`
66
+
54
67
  Schemas are mirrored in `lib/mcp/tool-manifests/kynver-mcp-agent-os.json` in
55
68
  the Kynver repo (used as the static fallback when stdio spawn isn't available
56
69
  — e.g. on Vercel).
@@ -63,6 +76,12 @@ state to important memories instead of burying those pointers in freeform text.
63
76
  Set `reviewStatus: "needs_review"` for important or uncertain memories that a
64
77
  human should be able to find and correct in the dashboard.
65
78
 
79
+ Active-state and correction tools expose the "what is true right now" layer:
80
+ `agent_os_record_state` / `agent_os_get_state` manage one current
81
+ `active_state` row per live PR, branch, or deployment, while
82
+ `agent_os_correct_memory` writes an auditable correction that supersedes stale
83
+ or wrong remembered claims instead of silently editing history.
84
+
66
85
  Skill tools expose the AgentOS skill registry and skill-tagged memory contract.
67
86
  Use `agent_os_get_skill` to fetch instructions on demand, `agent_os_create_skill`
68
87
  /`agent_os_update_skill` to manage user-authored skills, `agent_os_import_skills`
@@ -75,6 +94,18 @@ slug as `skillId`; user-authored skill memories use `AgentSkill.id`.
75
94
  summaries can then be synthesized from actual topics, actions, decisions, and
76
95
  follow-ups instead of generic lifecycle text.
77
96
 
97
+ AgentPlan tools manage first-class plans — versioned operational artifacts
98
+ distinct from goals/projects/tasks. Each `AgentPlan` owns a chain of immutable
99
+ `AgentPlanVersion` snapshots (one stamped current at a time) plus typed
100
+ `AgentPlanLink` rows pointing at goals/projects/tasks/memories/sessions/plans
101
+ or remote refs (repo/branch/commit/PR/URL/markdown). The plan body itself is
102
+ immutable — to record a new snapshot call `agent_os_plan_add_version`, which
103
+ appends a new version and (by default) advances `currentVersionId` atomically.
104
+ Use `planSlug` for the plan's own stable slug; the workspace `slug` argument
105
+ is reserved for the AgentOS workspace. Workspace-owned link targets are
106
+ re-validated server-side against the same AgentOS workspace; remote refs
107
+ carry metadata only.
108
+
78
109
  AgentTask tools expose the durable task board. `agent_os_task_create` is
79
110
  idempotent within an AgentOS — pass `idempotencyKey` (or `requestId`) from the
80
111
  foreground turn so a retry or restart returns the existing task instead of
package/dist/server.js CHANGED
@@ -9,7 +9,9 @@
9
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { z } from "zod";
11
11
  import { get, post, patch } from "./lib/kynver-client.js";
12
+ import { registerPlanTools } from "./tools/plans.js";
12
13
  import { registerTaskTools } from "./tools/tasks.js";
14
+ import { registerActionRequiredTools } from "./tools/action-required.js";
13
15
  function jsonResult(data) {
14
16
  return {
15
17
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
@@ -82,7 +84,7 @@ export function createAgentOsServer() {
82
84
  })
83
85
  .passthrough();
84
86
  const memoryTypeSchema = z
85
- .enum(["decision", "lesson", "fact", "preference", "project_update", "runbook", "source_map", "correction", "note"])
87
+ .enum(["decision", "lesson", "fact", "preference", "project_update", "runbook", "source_map", "correction", "active_state", "note"])
86
88
  .describe("What kind of durable memory this is. Use note only when a more specific type does not fit.");
87
89
  const confidenceSchema = z
88
90
  .enum(["low", "medium", "high"])
@@ -90,6 +92,9 @@ export function createAgentOsServer() {
90
92
  const reviewStatusSchema = z
91
93
  .enum(["none", "needs_review", "reviewed", "stale", "rejected"])
92
94
  .describe("Set needs_review for important, uncertain, stale-looking, or user-correctable memories.");
95
+ const activeStateEntityTypeSchema = z
96
+ .enum(["pr_watch", "branch", "deployment"])
97
+ .describe("Tracked entity kind. pr_watch: a live PR. branch: local-vs-remote branch state. deployment: a live release.");
93
98
  const skillCategorySchema = z.enum(["engineering", "productivity", "agent_ops", "domain", "personal", "misc"]);
94
99
  const skillStatusSchema = z.enum(["draft", "active", "deprecated", "archived"]);
95
100
  const skillDraftSchema = z.object({
@@ -116,6 +121,26 @@ export function createAgentOsServer() {
116
121
  const s = await resolveToolSlug(args.slug);
117
122
  return jsonResult(await get(`/agent-os/${s}/stats`));
118
123
  });
124
+ register("agent_os_context_envelope", "Build a compact context envelope for one anchor (plan | task | goal | project | session). Returns the resolved anchor, its goal + plan (with current version body) + most-relevant task + recent session + top related memories with deduplicated source refs. Use this instead of reading giant docs when picking up an in-flight piece of work — one round trip, scoped to your workspace.", {
125
+ anchorType: z.enum(["plan", "task", "goal", "project", "session"]).describe("Kind of anchor to load context around."),
126
+ anchorId: z.string().describe("Id of the anchor row within this workspace."),
127
+ memoryQuery: z.string().optional().describe("Override the memory search query. Defaults to a query derived from the anchor's title/summary."),
128
+ memoryLimit: z.number().optional().describe("Max memory hits (default 5, max 20). Set 0 to skip memory recall."),
129
+ sessionLimit: z.number().optional().describe("Max recent sessions (default 1, max 5). Set 0 to skip."),
130
+ slug: slugField,
131
+ }, async (args) => {
132
+ const s = await resolveToolSlug(args.slug);
133
+ const params = new URLSearchParams();
134
+ params.set("anchorType", args.anchorType);
135
+ params.set("anchorId", args.anchorId);
136
+ if (args.memoryQuery)
137
+ params.set("memoryQuery", args.memoryQuery);
138
+ if (args.memoryLimit !== undefined)
139
+ params.set("memoryLimit", String(args.memoryLimit));
140
+ if (args.sessionLimit !== undefined)
141
+ params.set("sessionLimit", String(args.sessionLimit));
142
+ return jsonResult(await get(`/agent-os/${s}/context-envelope?${params}`));
143
+ });
119
144
  register("agent_os_open_session", "Call this at the START of every session. Records channel, model, and opens a session record for continuity. Ghost calls this immediately after agent_os_get_context on startup.", {
120
145
  channel: z.string().describe("Runtime channel: 'webchat' | 'telegram' | 'discord' | …"),
121
146
  model: z.string().optional().describe("Model used for the session (default 'claude-sonnet-4-6')."),
@@ -244,14 +269,31 @@ export function createAgentOsServer() {
244
269
  register("agent_os_search_memory", "Semantic + keyword hybrid search over the agent's long-term MARM memory (ownerType=agent). Use to recall specific decisions, project details, or lessons without loading full context. Replaces grep/reading MEMORY.md. Results include rrfScore — a Reciprocal Rank Fusion score where ~0.033 is a near-perfect match. Higher is better; do not treat low numbers as poor results.", {
245
270
  query: z.string().describe("Natural language query to search memory."),
246
271
  k: z.number().optional().describe("Number of results (default 10, max 50)."),
272
+ ticker: z.string().optional().describe("Trading desk: filter hits to this ticker symbol."),
273
+ debatePersona: z.enum(["bull", "bear"]).optional().describe("Trading desk: filter to bull or bear advocate memories."),
247
274
  slug: slugField,
248
275
  }, async (args) => {
249
276
  const params = new URLSearchParams({ q: args.query });
250
277
  if (args.k)
251
278
  params.set("k", String(args.k));
279
+ if (args.ticker)
280
+ params.set("ticker", args.ticker);
281
+ if (args.debatePersona)
282
+ params.set("debatePersona", args.debatePersona);
252
283
  const s = await resolveToolSlug(args.slug);
253
284
  return jsonResult(await get(`/agent-os/${s}/memory?${params}`));
254
285
  });
286
+ register("agent_os_get_desk_debate_context", "Fetch trading-desk debate context for a ticker: latest canonical DeskAnalysisReport debate fields plus bull/bear MARM snippets scoped by ticker and debatePersona.", {
287
+ ticker: z.string().describe("Equity ticker symbol, e.g. NVDA."),
288
+ reportId: z.string().optional().describe("Optional DeskAnalysisReport id; defaults to latest canonical report for the ticker."),
289
+ slug: slugField,
290
+ }, async (args) => {
291
+ const params = new URLSearchParams({ ticker: args.ticker });
292
+ if (args.reportId)
293
+ params.set("reportId", args.reportId);
294
+ const s = await resolveToolSlug(args.slug);
295
+ return jsonResult(await get(`/agent-os/${s}/desk-debate-context?${params}`));
296
+ });
255
297
  register("agent_os_list_skills", "List AgentOS skills available to this workspace, including Kynver built-ins, user-authored skills, and per-AgentOS binding state. Pass view=\"manifest\" for the runtime read path: only enabled, runtime-eligible skills (enabled binding plus, for user skills, status=active) with metadata only — no instructions. Skill instructions are fetched on demand with agent_os_get_skill; do not inject every skill into every model context.", {
256
298
  view: z
257
299
  .enum(["full", "manifest"])
@@ -504,7 +546,71 @@ export function createAgentOsServer() {
504
546
  const s = await resolveToolSlug(slug);
505
547
  return jsonResult(await post(`/agent-os/${s}/memory`, body));
506
548
  });
549
+ register("agent_os_correct_memory", "Write an auditable correction memory that supersedes a stale or wrong memory by slug. Use when fixing an incorrect remembered claim rather than silently editing history.", {
550
+ targetSlug: z.string().describe("Slug/key of the memory being corrected."),
551
+ content: z.string().describe("Correct replacement content to remember."),
552
+ reason: z.string().describe("Why the target is wrong; recorded on the superseded target."),
553
+ key: z.string().optional().describe("Slug for the new correction memory. Defaults to <targetSlug>-correction."),
554
+ alsoInvalidates: z.array(z.string()).optional().describe("Additional memory slugs/ids to supersede alongside targetSlug."),
555
+ assertedBySessionId: z.string().optional(),
556
+ sourceId: z.string().optional(),
557
+ sourceRefs: z.array(sourceRefSchema).optional(),
558
+ memoryType: memoryTypeSchema.optional(),
559
+ confidence: confidenceSchema.optional(),
560
+ reviewStatus: reviewStatusSchema.optional(),
561
+ projectId: z.string().optional(),
562
+ goalId: z.string().optional(),
563
+ contactId: z.string().optional(),
564
+ skillId: z.string().optional(),
565
+ slug: slugField,
566
+ }, async (args) => {
567
+ const { slug, ...body } = args;
568
+ const s = await resolveToolSlug(slug);
569
+ return jsonResult(await post("/agent-os/" + s + "/memory/correct", body));
570
+ });
571
+ register("agent_os_record_state", "Record current active state for a live mutable entity such as a PR, branch, or deployment. Server derives the canonical entityKey from fields and upserts one active_state row per entity.", {
572
+ entityType: activeStateEntityTypeSchema,
573
+ fields: z.record(z.string(), z.unknown()).describe("Entity-specific current truth. pr_watch requires repo, pr, author, headOwner, headBranch, state. branch requires repo, branch, aheadBy, behindBy, lastCommit. deployment requires target, env, state, url, and version or sha."),
574
+ observedAt: z.string().optional().describe("ISO timestamp the state was observed. Defaults to now."),
575
+ content: z.string().optional().describe("Optional human summary; one is generated otherwise."),
576
+ sourceRefs: z.array(sourceRefSchema).optional(),
577
+ projectId: z.string().optional(),
578
+ confidence: confidenceSchema.optional(),
579
+ reviewStatus: reviewStatusSchema.optional(),
580
+ sourceId: z.string().optional(),
581
+ metadata: z.record(z.string(), z.unknown()).optional(),
582
+ slug: slugField,
583
+ }, async (args) => {
584
+ const { slug, ...body } = args;
585
+ const s = await resolveToolSlug(slug);
586
+ return jsonResult(await post("/agent-os/" + s + "/memory/state", body));
587
+ });
588
+ register("agent_os_get_state", "Get current active state records. Pass entityKey for one record, or entityType/projectId/limit to list active PR, branch, or deployment state.", {
589
+ entityKey: z.string().optional().describe("Canonical key of a single record, e.g. pr:openclaw/openclaw#83529. Omit to list records."),
590
+ entityType: activeStateEntityTypeSchema.optional(),
591
+ projectId: z.string().optional(),
592
+ limit: z.number().optional(),
593
+ slug: slugField,
594
+ }, async (args) => {
595
+ const { slug, entityKey, entityType, projectId, limit } = args;
596
+ const s = await resolveToolSlug(slug);
597
+ const qs = new URLSearchParams();
598
+ if (entityKey)
599
+ qs.set("entityKey", entityKey);
600
+ if (entityType)
601
+ qs.set("entityType", entityType);
602
+ if (projectId)
603
+ qs.set("projectId", projectId);
604
+ if (limit !== undefined)
605
+ qs.set("limit", String(limit));
606
+ const suffix = qs.toString() ? "?" + qs.toString() : "";
607
+ return jsonResult(await get("/agent-os/" + s + "/memory/state" + suffix));
608
+ });
507
609
  // AgentTask board — batch 1 (CRUD) + batch 1.5 (steering / lease) tools.
508
610
  registerTaskTools({ register, resolveToolSlug, jsonResult, compact });
611
+ // First-class AgentOS plans (Phase 5A): versioned operational artifacts.
612
+ registerPlanTools({ register, resolveToolSlug, jsonResult, compact });
613
+ // Action Required inbox: agent_os_ask_human + agent_os_await_action (slice 1).
614
+ registerActionRequiredTools({ register, jsonResult, compact });
509
615
  return server;
510
616
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Action Required inbox MCP tools.
3
+ *
4
+ * Slice 1 exposes two tools:
5
+ * - agent_os_ask_human : create one inbox item (one decision per call)
6
+ * - agent_os_await_action: poll an item until it resolves (or timeout)
7
+ *
8
+ * Each tool proxies to `/api/action-required/*`. The Kynver API enforces
9
+ * per-user ownership and the one-question lint server-side.
10
+ */
11
+ type ToolResult = {
12
+ content: {
13
+ type: "text";
14
+ text: string;
15
+ }[];
16
+ structuredContent?: unknown;
17
+ };
18
+ export interface ActionRequiredToolDeps {
19
+ register: (name: string, description: string, inputSchema: Record<string, unknown>, cb: (args: any) => any) => void;
20
+ jsonResult: (data: unknown) => ToolResult;
21
+ compact: (source: Record<string, unknown>) => Record<string, unknown>;
22
+ }
23
+ export declare function registerActionRequiredTools({ register, jsonResult, compact, }: ActionRequiredToolDeps): void;
24
+ export {};
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Action Required inbox MCP tools.
3
+ *
4
+ * Slice 1 exposes two tools:
5
+ * - agent_os_ask_human : create one inbox item (one decision per call)
6
+ * - agent_os_await_action: poll an item until it resolves (or timeout)
7
+ *
8
+ * Each tool proxies to `/api/action-required/*`. The Kynver API enforces
9
+ * per-user ownership and the one-question lint server-side.
10
+ */
11
+ import { z } from "zod";
12
+ import { get, post } from "../lib/kynver-client.js";
13
+ const category = z.enum([
14
+ "decision",
15
+ "approval",
16
+ "question",
17
+ "blocker",
18
+ "clarification",
19
+ ]);
20
+ const priority = z.enum(["low", "normal", "high", "critical"]);
21
+ const blockerKind = z
22
+ .enum([
23
+ "none",
24
+ "deploy",
25
+ "env",
26
+ "db",
27
+ "npm",
28
+ "live_trade",
29
+ "content_publish",
30
+ "code_review",
31
+ "merge",
32
+ "other",
33
+ ])
34
+ .optional();
35
+ const askerKind = z.enum(["agent", "reviewer", "task", "system"]).default("agent");
36
+ const recommendation = z
37
+ .object({
38
+ value: z.string().describe("The recommended answer the user can accept with one tap."),
39
+ confidence: z.enum(["low", "medium", "high"]).optional(),
40
+ rationale: z.string().optional(),
41
+ source: z.enum(["agent", "policy", "history"]).optional(),
42
+ })
43
+ .strict();
44
+ const evidence = z
45
+ .array(z
46
+ .object({
47
+ kind: z.enum(["url", "memory", "task_event", "file", "text", "pr", "commit"]),
48
+ label: z.string().optional(),
49
+ ref: z.string(),
50
+ })
51
+ .strict())
52
+ .optional();
53
+ const options = z
54
+ .array(z
55
+ .object({
56
+ value: z.string(),
57
+ label: z.string().optional(),
58
+ isRecommended: z.boolean().optional(),
59
+ })
60
+ .strict())
61
+ .optional();
62
+ export function registerActionRequiredTools({ register, jsonResult, compact, }) {
63
+ register("agent_os_ask_human", [
64
+ "Surface one decision to the human via the Action Required inbox.",
65
+ "Each call MUST contain exactly one question the human can answer in one breath.",
66
+ "Compound asks ('Do X? Or Y?') are rejected server-side with multiple_questions_detected — split them into two calls.",
67
+ "Recommendation is required for category=decision|approval; supply the best-guess answer the user can one-tap accept.",
68
+ "Returns the created `item` (with id you can pass to agent_os_await_action) and a `created` flag (false = idempotent re-create).",
69
+ ].join(" "), {
70
+ title: z.string().describe("Short headline rendered as the card title."),
71
+ question: z
72
+ .string()
73
+ .describe("The single question (one '?'). The user sees this verbatim."),
74
+ body: z
75
+ .string()
76
+ .optional()
77
+ .describe("Expanded context / background. Collapsed in the UI; expand-on-tap."),
78
+ category: category.default("decision"),
79
+ priority: priority.default("normal"),
80
+ askerKind: askerKind.optional(),
81
+ askerRef: z.string().optional().describe("Free-form ref (e.g. agent slug, reviewer id)."),
82
+ blockerKind: blockerKind,
83
+ recommendation: recommendation.optional(),
84
+ evidence,
85
+ options,
86
+ taskId: z.string().optional().describe("Linked AgentTask id — unblocks on answer."),
87
+ planId: z.string().optional(),
88
+ planVersionId: z.string().optional(),
89
+ goalId: z.string().optional(),
90
+ projectId: z.string().optional(),
91
+ sessionId: z.string().optional(),
92
+ sourceEventId: z.number().int().optional(),
93
+ unblockTaskIds: z.array(z.string()).optional(),
94
+ expiresAt: z
95
+ .string()
96
+ .optional()
97
+ .describe("ISO-8601 timestamp for hard deadline. Defaults to 24h for approvals; null otherwise."),
98
+ agentOsId: z.string().optional(),
99
+ createdByAgentRef: z.string().optional(),
100
+ clientRequestId: z
101
+ .string()
102
+ .optional()
103
+ .describe("Dedupe key — repeating the same id within a user returns the existing item with created=false."),
104
+ }, async (args) => {
105
+ return jsonResult(await post("/action-required", compact(args)));
106
+ });
107
+ register("agent_os_await_action", [
108
+ "Poll one Action Required item until it resolves.",
109
+ "Returns immediately if the item is already terminal (answered / resolved_noop / expired / superseded / cancelled).",
110
+ "If still open/snoozed, waits up to `timeoutSeconds` (default 60, max 300) with `pollIntervalSeconds` between checks.",
111
+ "On answer, the answer text + acceptedRecommendation flag are returned so you can resume work without re-fetching.",
112
+ ].join(" "), {
113
+ itemId: z.string().describe("The Action Required item id returned by agent_os_ask_human."),
114
+ timeoutSeconds: z.number().int().min(0).max(300).optional(),
115
+ pollIntervalSeconds: z.number().int().min(2).max(60).optional(),
116
+ }, async (args) => {
117
+ const itemId = String(args.itemId);
118
+ const timeout = Math.min(300, Math.max(0, args.timeoutSeconds ?? 60));
119
+ const interval = Math.max(2, Math.min(60, args.pollIntervalSeconds ?? 5));
120
+ const start = Date.now();
121
+ let last = null;
122
+ while (true) {
123
+ const data = (await get(`/action-required/${encodeURIComponent(itemId)}/await`));
124
+ last = data;
125
+ if (data.status !== "open" && data.status !== "snoozed") {
126
+ return jsonResult({ ...data, waited: Math.round((Date.now() - start) / 1000) });
127
+ }
128
+ if (Date.now() - start >= timeout * 1000) {
129
+ return jsonResult({
130
+ ...data,
131
+ stillOpen: true,
132
+ waited: Math.round((Date.now() - start) / 1000),
133
+ });
134
+ }
135
+ await new Promise((r) => setTimeout(r, interval * 1000));
136
+ }
137
+ // eslint-disable-next-line no-unreachable
138
+ return jsonResult(last);
139
+ });
140
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * AgentOS plan MCP tools — Phase 5A of
3
+ * docs/superpowers/plans/2026-05-17-agent-os-memory-reliability.md. Each tool
4
+ * proxies to `/api/agent-os/{slug}/plans*` on Kynver. Workspace isolation is
5
+ * enforced server-side via slug-resolved AgentOS context plus service-layer
6
+ * scope checks — callers do NOT pass userId or agentOsId from here.
7
+ *
8
+ * Kept in its own module so server.ts stays small. server.ts calls
9
+ * `registerPlanTools` with the helpers it already has in scope.
10
+ */
11
+ type ToolResult = {
12
+ content: {
13
+ type: "text";
14
+ text: string;
15
+ }[];
16
+ structuredContent?: unknown;
17
+ };
18
+ export interface PlanToolDeps {
19
+ /** server.ts's thin wrapper over `server.registerTool`. */
20
+ register: (name: string, description: string, inputSchema: Record<string, unknown>, cb: (args: any) => any) => void;
21
+ /** Resolve the effective AgentOS slug (explicit arg → env → discovered primary). */
22
+ resolveToolSlug: (explicitSlug?: string) => Promise<string>;
23
+ /** Wrap a value as an MCP text + structuredContent result. */
24
+ jsonResult: (data: unknown) => ToolResult;
25
+ /** Drop `undefined` entries from an object. */
26
+ compact: (source: Record<string, unknown>) => Record<string, unknown>;
27
+ }
28
+ export declare function registerPlanTools({ register, resolveToolSlug, jsonResult, compact, }: PlanToolDeps): void;
29
+ export {};
@@ -0,0 +1,201 @@
1
+ /**
2
+ * AgentOS plan MCP tools — Phase 5A of
3
+ * docs/superpowers/plans/2026-05-17-agent-os-memory-reliability.md. Each tool
4
+ * proxies to `/api/agent-os/{slug}/plans*` on Kynver. Workspace isolation is
5
+ * enforced server-side via slug-resolved AgentOS context plus service-layer
6
+ * scope checks — callers do NOT pass userId or agentOsId from here.
7
+ *
8
+ * Kept in its own module so server.ts stays small. server.ts calls
9
+ * `registerPlanTools` with the helpers it already has in scope.
10
+ */
11
+ import { z } from "zod";
12
+ import { get, post, patch } from "../lib/kynver-client.js";
13
+ const planStatus = z.enum([
14
+ "draft",
15
+ "active",
16
+ "blocked",
17
+ "complete",
18
+ "archived",
19
+ "superseded",
20
+ ]);
21
+ const planPriority = z.enum(["low", "normal", "high", "critical"]);
22
+ const planLinkTarget = z.enum([
23
+ "goal",
24
+ "project",
25
+ "task",
26
+ "memory",
27
+ "session",
28
+ "repo",
29
+ "branch",
30
+ "commit",
31
+ "pr",
32
+ "url",
33
+ "markdown",
34
+ "plan",
35
+ ]);
36
+ const initialVersionSchema = z
37
+ .object({
38
+ title: z.string().optional(),
39
+ body: z.string().describe("Full plan body (markdown)."),
40
+ summary: z.string().optional(),
41
+ changeSummary: z.string().optional(),
42
+ author: z.string().optional(),
43
+ sourceRefs: z.unknown().optional(),
44
+ metadata: z.record(z.string(), z.unknown()).optional(),
45
+ })
46
+ .passthrough();
47
+ export function registerPlanTools({ register, resolveToolSlug, jsonResult, compact, }) {
48
+ const slugField = z
49
+ .string()
50
+ .optional()
51
+ .describe("AgentOS workspace slug. Omit to use env or the server-discovered primary.");
52
+ register("agent_os_plan_create", "Create a first-class AgentOS plan with an initial version. Plans are versioned operational artifacts (separate from goals/projects/tasks). `initialVersion.body` is required and becomes the plan's current version. Use `planSlug` for the plan's own stable slug — the workspace `slug` argument is reserved for the AgentOS workspace.", {
53
+ title: z.string().describe("Human-readable plan title (required)."),
54
+ summary: z.string().optional(),
55
+ planSlug: z
56
+ .string()
57
+ .optional()
58
+ .describe("Stable slug for the plan within the AgentOS workspace."),
59
+ status: planStatus.optional(),
60
+ priority: planPriority.optional(),
61
+ owner: z.string().optional(),
62
+ projectId: z.string().optional(),
63
+ goalId: z.string().optional(),
64
+ canonicalKey: z.string().optional(),
65
+ metadata: z.record(z.string(), z.unknown()).optional(),
66
+ sourceRefs: z.unknown().optional(),
67
+ initialVersion: initialVersionSchema.describe("First version snapshot (required). Becomes the plan's current version."),
68
+ slug: slugField,
69
+ }, async (args) => {
70
+ const { slug, planSlug, ...rest } = args;
71
+ const s = await resolveToolSlug(slug);
72
+ const body = compact({
73
+ ...rest,
74
+ ...(planSlug !== undefined ? { slug: planSlug } : {}),
75
+ });
76
+ return jsonResult(await post(`/agent-os/${s}/plans`, body));
77
+ });
78
+ register("agent_os_plan_list", "List AgentOS plans, optionally filtered by status, projectId, goalId, or canonicalKey.", {
79
+ status: planStatus.optional(),
80
+ projectId: z.string().optional(),
81
+ goalId: z.string().optional(),
82
+ canonicalKey: z.string().optional(),
83
+ limit: z
84
+ .number()
85
+ .optional()
86
+ .describe("Max rows (default 50, max 200)."),
87
+ slug: slugField,
88
+ }, async (args) => {
89
+ const params = new URLSearchParams();
90
+ if (args.status)
91
+ params.set("status", args.status);
92
+ if (args.projectId)
93
+ params.set("projectId", args.projectId);
94
+ if (args.goalId)
95
+ params.set("goalId", args.goalId);
96
+ if (args.canonicalKey)
97
+ params.set("canonicalKey", args.canonicalKey);
98
+ if (args.limit)
99
+ params.set("limit", String(args.limit));
100
+ const qs = params.toString();
101
+ const s = await resolveToolSlug(args.slug);
102
+ return jsonResult(await get(`/agent-os/${s}/plans${qs ? `?${qs}` : ""}`));
103
+ });
104
+ register("agent_os_plan_get", "Fetch one AgentOS plan with its full version history, current version pointer, and typed links.", { planId: z.string(), slug: slugField }, async (args) => {
105
+ const s = await resolveToolSlug(args.slug);
106
+ return jsonResult(await get(`/agent-os/${s}/plans/${args.planId}`));
107
+ });
108
+ register("agent_os_plan_update", "Patch an AgentOS plan's metadata (title, summary, status, priority, owner, project/goal links, canonicalKey). The plan body itself is immutable — use agent_os_plan_add_version to record a new snapshot. Use `planSlug` for the plan's own stable slug.", {
109
+ planId: z.string(),
110
+ title: z.string().optional(),
111
+ summary: z.string().optional(),
112
+ planSlug: z
113
+ .string()
114
+ .optional()
115
+ .describe("Stable slug for the plan within the AgentOS workspace."),
116
+ status: planStatus.optional(),
117
+ priority: planPriority.optional(),
118
+ owner: z.string().optional(),
119
+ projectId: z.string().optional(),
120
+ goalId: z.string().optional(),
121
+ canonicalKey: z.string().optional(),
122
+ metadata: z.record(z.string(), z.unknown()).optional(),
123
+ sourceRefs: z.unknown().optional(),
124
+ slug: slugField,
125
+ }, async (args) => {
126
+ const { slug, planId, planSlug, ...rest } = args;
127
+ const s = await resolveToolSlug(slug);
128
+ const body = compact({
129
+ ...rest,
130
+ ...(planSlug !== undefined ? { slug: planSlug } : {}),
131
+ });
132
+ return jsonResult(await patch(`/agent-os/${s}/plans/${planId}`, body));
133
+ });
134
+ register("agent_os_plan_add_version", "Append an immutable version snapshot to a plan. The new version's versionNumber is one greater than the latest, and (when markCurrent is true, the default) the plan's currentVersionId pointer advances atomically. Use this — not agent_os_plan_update — to record a new plan body.", {
135
+ planId: z.string(),
136
+ title: z.string().optional(),
137
+ body: z.string().describe("Full new version body (markdown)."),
138
+ summary: z.string().optional(),
139
+ changeSummary: z.string().optional(),
140
+ author: z.string().optional(),
141
+ sourceRefs: z.unknown().optional(),
142
+ metadata: z.record(z.string(), z.unknown()).optional(),
143
+ markCurrent: z
144
+ .boolean()
145
+ .optional()
146
+ .describe("When true (default), the new version becomes the plan's current version."),
147
+ slug: slugField,
148
+ }, async (args) => {
149
+ const { slug, planId, ...rest } = args;
150
+ const s = await resolveToolSlug(slug);
151
+ return jsonResult(await post(`/agent-os/${s}/plans/${planId}/versions`, compact(rest)));
152
+ });
153
+ register("agent_os_plan_mark_current", "Promote an existing version of a plan to be the current version. The version must already belong to the plan.", {
154
+ planId: z.string(),
155
+ versionId: z
156
+ .string()
157
+ .describe("Existing version id to promote to current."),
158
+ slug: slugField,
159
+ }, async (args) => {
160
+ const s = await resolveToolSlug(args.slug);
161
+ return jsonResult(await post(`/agent-os/${s}/plans/${args.planId}/versions`, {
162
+ action: "mark_current",
163
+ versionId: args.versionId,
164
+ }));
165
+ });
166
+ register("agent_os_plan_add_link", "Add a typed link from a plan (and optionally a specific version) to a related goal, project, task, memory, session, repo, branch, commit, PR, URL, markdown file, or other plan. Workspace-owned targets (goal/project/task/memory/session/plan) are validated against the same AgentOS workspace server-side; remote refs (url/repo/branch/commit/pr/markdown) carry metadata only.", {
167
+ planId: z.string(),
168
+ planVersionId: z
169
+ .string()
170
+ .optional()
171
+ .describe("Optional version id when the link applies to a specific snapshot."),
172
+ targetType: planLinkTarget,
173
+ targetId: z
174
+ .string()
175
+ .optional()
176
+ .describe("AgentOS-owned target id. Required for workspace-owned link types."),
177
+ targetUrl: z
178
+ .string()
179
+ .optional()
180
+ .describe("URL for remote refs. Either targetId or targetUrl is required."),
181
+ label: z.string().optional(),
182
+ relation: z
183
+ .string()
184
+ .optional()
185
+ .describe("Optional free-form relation label (e.g. 'derived_from', 'supersedes')."),
186
+ metadata: z.record(z.string(), z.unknown()).optional(),
187
+ slug: slugField,
188
+ }, async (args) => {
189
+ const { slug, planId, ...rest } = args;
190
+ const s = await resolveToolSlug(slug);
191
+ return jsonResult(await post(`/agent-os/${s}/plans/${planId}/links`, compact(rest)));
192
+ });
193
+ register("agent_os_plan_list_links", "List the typed links attached to a plan.", { planId: z.string(), slug: slugField }, async (args) => {
194
+ const s = await resolveToolSlug(args.slug);
195
+ return jsonResult(await get(`/agent-os/${s}/plans/${args.planId}/links`));
196
+ });
197
+ register("agent_os_plan_list_versions", "List the version history of a plan in ascending order by versionNumber. agent_os_plan_get already returns versions; use this when you only need the version list.", { planId: z.string(), slug: slugField }, async (args) => {
198
+ const s = await resolveToolSlug(args.slug);
199
+ return jsonResult(await get(`/agent-os/${s}/plans/${args.planId}/versions`));
200
+ });
201
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kynver-app/mcp-agent-os",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Kynver Agentic OS MCP server — slug-keyed agent identity, goals, projects, sessions, and long-term memory",
5
5
  "type": "module",
6
6
  "bin": {