@aioproductoscom/mcp 0.15.10 → 0.16.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/dist/client.d.ts +16 -0
- package/dist/client.js +30 -347
- package/dist/index.d.ts +2 -0
- package/dist/index.js +19 -379
- package/dist/registry.d.ts +63 -0
- package/dist/registry.js +1074 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +128 -0
- package/dist/ui-views.d.ts +15 -0
- package/dist/ui-views.js +71 -0
- package/dist/update-notifier.d.ts +6 -0
- package/dist/update-notifier.js +1 -1
- package/package.json +20 -6
- package/dist/pm-skill.js +0 -40
package/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
// Bin entry for the stdio MCP. Everything of substance lives elsewhere:
|
|
3
|
+
// registry.ts (the shared tool source of truth, also served by the hosted
|
|
4
|
+
// connector), server.ts (the transport-agnostic server factory), client.ts
|
|
5
|
+
// (the one hardened HTTP executor). This file wires env → client → server →
|
|
6
|
+
// stdio, plus the once-only update banner.
|
|
3
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { z } from "zod";
|
|
5
8
|
import { PlatformClient } from "./client.js";
|
|
6
|
-
import {
|
|
9
|
+
import { createPmServer } from "./server.js";
|
|
7
10
|
import { checkForUpdate, updateBanner } from "./update-notifier.js";
|
|
8
|
-
const VERSION = "0.
|
|
11
|
+
const VERSION = "0.16.0";
|
|
9
12
|
const token = process.env.PRODUCTOS_TOKEN;
|
|
10
13
|
const baseUrl = (process.env.PRODUCTOS_URL ?? "https://platform.aioproductos.com").replace(/\/$/, "");
|
|
11
14
|
if (!token) {
|
|
@@ -17,383 +20,20 @@ const client = new PlatformClient(baseUrl, token);
|
|
|
17
20
|
// on the NEXT tool result (the model reads it and tells the user) — exactly once.
|
|
18
21
|
let updateNotice = null;
|
|
19
22
|
let updateNoticeShown = false;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
/** A prompt result — one user-role message the host injects when the slash
|
|
31
|
-
* command runs. Surfaces as a named slash command in the AI host. */
|
|
32
|
-
function promptMsg(body) {
|
|
33
|
-
return { messages: [{ role: "user", content: { type: "text", text: body } }] };
|
|
34
|
-
}
|
|
35
|
-
const server = new McpServer({ name: "AIOProductOS", version: VERSION }, {
|
|
36
|
-
instructions: "You manage product work on AIOProductOS for the connected member — board, insights, features, and " +
|
|
37
|
-
"analytics all hang off one spine. Call get_pm_playbook first for how to operate: ground in " +
|
|
38
|
-
"get_product_brain, keep work tied to the spine (insight→feature→task→outcome), and prioritize on " +
|
|
39
|
-
"evidence (affected accounts + MRR + reach), never an invented score. Resolve names to ids with " +
|
|
40
|
-
"pm_meta before create_task / update_task; never guess an id. Confirm what you changed in plain " +
|
|
41
|
-
"language, and claim only writes you actually made.",
|
|
23
|
+
const server = createPmServer({
|
|
24
|
+
version: VERSION,
|
|
25
|
+
request: (call) => client.request(call),
|
|
26
|
+
decorate: (content) => {
|
|
27
|
+
if (updateNotice && !updateNoticeShown) {
|
|
28
|
+
updateNoticeShown = true;
|
|
29
|
+
return [...content, { type: "text", text: updateNotice }];
|
|
30
|
+
}
|
|
31
|
+
return content;
|
|
32
|
+
},
|
|
42
33
|
});
|
|
43
|
-
server.tool("whoami", "Show the connected AIOProductOS identity (org, member) and the org's products. Read-only; returns the identity plus the product list. For a multi-product org call this first to get product ids, then pass one as product_id to any product-scoped tool; omit product_id to use the primary.", {}, async () => text(await client.whoami()));
|
|
44
|
-
server.tool("get_pm_playbook", "How to operate as a product manager on AIOProductOS — ground in the brain, keep work tied to the spine (insight→feature→task→outcome), and prioritize on evidence (affected accounts + MRR + reach), never an invented score. Read this before planning or prioritizing.", {}, async () => text(`${PM_PLAYBOOK}\n\n— pm playbook v${PM_SKILL_VERSION}`));
|
|
45
|
-
server.tool("pm_meta", "List the org's PM lists, statuses, members, and features as id+name pairs. Read-only; returns arrays for name→id resolution. Call it before create_task / update_task to turn names into ids — never guess an id.", {}, async () => text(await client.meta()));
|
|
46
|
-
server.tool("list_tasks", "List the org's board tasks and return the matches with their status, priority, assignees, and any linked feature/insight. Read-only; returns an empty list when nothing matches. Optionally narrow by status_id or list_id (resolve either via pm_meta). Use it to find a task id before get_task, update_task, or comment_on_task.", {
|
|
47
|
-
status_id: z.string().optional().describe("Filter by status id, from pm_meta."),
|
|
48
|
-
list_id: z.string().optional().describe("Filter by list id, from pm_meta."),
|
|
49
|
-
}, async (a) => text(await client.listTasks(a)));
|
|
50
|
-
server.tool("get_task", "Get one task by id and return it with its full comments and assignees. Read-only. Resolve the id first with list_tasks — never guess it; pair with update_task or comment_on_task to act on what you read.", { id: z.string().describe("Task id, from list_tasks.") }, async (a) => text(await client.getTask(a.id)));
|
|
51
|
-
server.tool("create_task", "Create a task and return the created task. A write — each call creates a new task, so don't retry blindly. Omitting list_id uses the org's first list; feature_id / insight_id link it to the AIOProductOS spine. Resolve list/status/feature/insight/member ids via pm_meta first — never guess them. Only title is required.", {
|
|
52
|
-
title: z.string().describe("Task title."),
|
|
53
|
-
description: z.string().optional().describe("Task description (optional)."),
|
|
54
|
-
priority: z.enum(["urgent", "high", "normal", "low"]).optional().describe("Priority (optional)."),
|
|
55
|
-
list_id: z.string().optional().describe("List id, from pm_meta (optional; defaults to the org's first list)."),
|
|
56
|
-
status_id: z.string().optional().describe("Status id, from pm_meta (optional)."),
|
|
57
|
-
feature_id: z.string().optional().describe("Link to a feature, id from pm_meta (optional)."),
|
|
58
|
-
insight_id: z.string().optional().describe("Link to an insight (optional)."),
|
|
59
|
-
assignee_member_ids: z.array(z.string()).optional().describe("Member ids to assign, from pm_meta (optional)."),
|
|
60
|
-
}, async (a) => text(await client.createTask(a)));
|
|
61
|
-
server.tool("create_feature", "Create a feature on the product spine and return it (id, key, name, status). The key is generated from the name; status starts 'active'. product_id defaults to the org's primary product when omitted — pass one from whoami for a multi-product org. Only name is required; create the feature before linking tasks to it with create_task.", {
|
|
62
|
-
name: z.string().describe("Feature name (the only required field), e.g. 'SAML SSO'."),
|
|
63
|
-
description: z.string().optional().describe("What the feature is / why it matters (optional)."),
|
|
64
|
-
product_id: z.string().optional().describe("Product to create it under, from whoami (optional; the org's primary product when omitted)."),
|
|
65
|
-
initiative_id: z.string().optional().describe("Initiative to align this feature under for line-of-sight, from list_initiatives (optional)."),
|
|
66
|
-
objective_id: z.string().optional().describe("Objective (goal) to align this feature under directly when there's no intermediate initiative, from list_objectives (optional)."),
|
|
67
|
-
}, async (a) => text(await client.createFeature(a)));
|
|
68
|
-
server.tool("create_objective", "Create an objective (OKR), optionally with key results, and return it. period is free text (e.g. 'Q3 2026'); product_id and parent_id (a parent objective) are optional and verified in-org. Each key result takes name + optional unit / start_value / target_value. Only name is required.", {
|
|
69
|
-
name: z.string().describe("Objective name (the only required field), e.g. 'Reach $50k MRR'."),
|
|
70
|
-
description: z.string().optional().describe("Context for the objective (optional)."),
|
|
71
|
-
period: z.string().optional().describe("Free-text period, e.g. 'Q3 2026' (optional)."),
|
|
72
|
-
product_id: z.string().optional().describe("Product to scope it to, from whoami (optional)."),
|
|
73
|
-
parent_id: z.string().optional().describe("Parent objective id to nest under (optional)."),
|
|
74
|
-
key_results: z
|
|
75
|
-
.array(z.object({
|
|
76
|
-
name: z.string().describe("Key result name, e.g. 'MRR'."),
|
|
77
|
-
unit: z.string().optional().describe("Unit, e.g. 'USD' or '%' (optional)."),
|
|
78
|
-
start_value: z.number().optional().describe("Starting value (optional; default 0)."),
|
|
79
|
-
target_value: z.number().optional().describe("Target value (optional)."),
|
|
80
|
-
}))
|
|
81
|
-
.optional()
|
|
82
|
-
.describe("Key results to attach (optional; up to 10)."),
|
|
83
|
-
}, async (a) => text(await client.createObjective(a)));
|
|
84
|
-
server.tool("create_sprint", "Create a sprint and return it (id, name, goal, state, dates). state is 'future' (default) or 'active'; start_date / end_date are optional ISO 8601. Only name is required. Schedule tasks into it by passing the returned sprint id as sprint_id on create_task / update_task.", {
|
|
85
|
-
name: z.string().describe("Sprint name (the only required field), e.g. 'Sprint 12'."),
|
|
86
|
-
goal: z.string().optional().describe("The sprint goal (optional)."),
|
|
87
|
-
state: z.enum(["future", "active"]).optional().describe("Lifecycle state (optional; default 'future')."),
|
|
88
|
-
start_date: z.string().optional().describe("Start, ISO 8601 e.g. '2026-07-15T00:00:00Z' (optional)."),
|
|
89
|
-
end_date: z.string().optional().describe("End, ISO 8601 (optional)."),
|
|
90
|
-
}, async (a) => text(await client.createSprint(a)));
|
|
91
|
-
server.tool("create_page", "Create a Page (in-product doc / PRD on the spine) and return it (id, title). `body` is plain text — blank-line-separated blocks become paragraphs; omit it for a blank page. title defaults to 'Untitled'. product_id / parent_id (a parent page) are optional and verified in-org.", {
|
|
92
|
-
title: z.string().optional().describe("Page title (optional; 'Untitled' when omitted)."),
|
|
93
|
-
body: z.string().optional().describe("Page content as plain text; blank lines separate paragraphs (optional)."),
|
|
94
|
-
icon: z.string().optional().describe("An emoji icon for the page (optional)."),
|
|
95
|
-
product_id: z.string().optional().describe("Product to scope it to, from whoami (optional)."),
|
|
96
|
-
parent_id: z.string().optional().describe("Parent page id to nest under (optional)."),
|
|
97
|
-
}, async (a) => text(await client.createPage(a)));
|
|
98
|
-
server.tool("update_feature", "Update a feature and return it; omitted fields are unchanged. status is 'active' | 'discovered' | 'archived' (there is NO 'shipped' status — set mark_shipped:true to stamp its ship date instead). target_date is 'YYYY-MM-DD' (or null to clear). Resolve the id via list_features; only id is required.", {
|
|
99
|
-
id: z.string().describe("Feature id to update, from list_features (required)."),
|
|
100
|
-
name: z.string().optional().describe("New name (optional)."),
|
|
101
|
-
description: z.string().nullable().optional().describe("New description; null clears it (optional)."),
|
|
102
|
-
status: z.enum(["active", "discovered", "archived"]).optional().describe("Lifecycle status (optional)."),
|
|
103
|
-
target_date: z.string().nullable().optional().describe("Target ship date 'YYYY-MM-DD', or null to clear (optional)."),
|
|
104
|
-
mark_shipped: z.boolean().optional().describe("true stamps the ship date now; false clears it (optional)."),
|
|
105
|
-
initiative_id: z.string().nullable().optional().describe("Align under this initiative (line-of-sight), from list_initiatives; null unlinks (optional)."),
|
|
106
|
-
objective_id: z.string().nullable().optional().describe("Align directly under this objective, from list_objectives; null unlinks (optional)."),
|
|
107
|
-
}, async (a) => text(await client.updateFeature(a)));
|
|
108
|
-
server.tool("update_objective", "Update an objective's name / description / period and return it; omitted fields are unchanged (null clears description or period). Resolve the id via list_objectives; only id is required. To move a key result's value use update_key_result.", {
|
|
109
|
-
id: z.string().describe("Objective id, from list_objectives (required)."),
|
|
110
|
-
name: z.string().optional().describe("New name (optional)."),
|
|
111
|
-
description: z.string().nullable().optional().describe("New description; null clears it (optional)."),
|
|
112
|
-
period: z.string().nullable().optional().describe("New period, e.g. 'Q4 2026'; null clears it (optional)."),
|
|
113
|
-
}, async (a) => text(await client.updateObjective(a)));
|
|
114
|
-
server.tool("update_key_result", "Update a key result — most often to move current_value as progress lands — and return it; omitted fields are unchanged. Resolve the id via list_objectives (each objective carries its key_results with ids). Only id is required.", {
|
|
115
|
-
id: z.string().describe("Key result id, from list_objectives (required)."),
|
|
116
|
-
current_value: z.number().optional().describe("New current value (optional)."),
|
|
117
|
-
target_value: z.number().nullable().optional().describe("New target value; null clears it (optional)."),
|
|
118
|
-
start_value: z.number().optional().describe("New starting baseline (optional)."),
|
|
119
|
-
name: z.string().optional().describe("New name (optional)."),
|
|
120
|
-
unit: z.string().nullable().optional().describe("New unit, e.g. 'USD'; null clears it (optional)."),
|
|
121
|
-
}, async (a) => text(await client.updateKeyResult(a)));
|
|
122
|
-
server.tool("list_initiatives", "List the org's initiatives — the strategic layer between goals and features (goal → initiative → feature → epic → release). Each returns its name, status, timeframe, the objective it rolls up to (if any), and its linked-feature count. Read-only. Resolve an initiative id here before create_feature / update_feature (initiative_id) or update_initiative.", {
|
|
123
|
-
product_id: z.string().optional().describe("Only initiatives for this product, from whoami (optional)."),
|
|
124
|
-
}, async (a) => text(await client.listInitiatives(a.product_id)));
|
|
125
|
-
server.tool("create_initiative", "Create an initiative — a strategic effort that groups features and rolls up to a goal — and return it. Link it to a goal with objective_id (from list_objectives) to build line-of-sight. status is planned|active|paused|done|abandoned (default planned); timeframe is free text ('H2 2026'). product_id defaults to the primary product. Only name is required. Then align features to it via create_feature / update_feature (initiative_id).", {
|
|
126
|
-
name: z.string().describe("Initiative name (the only required field), e.g. 'Win enterprise'."),
|
|
127
|
-
description: z.string().optional().describe("What the initiative is / why it matters (optional)."),
|
|
128
|
-
objective_id: z.string().optional().describe("Goal this rolls up to, from list_objectives (optional; builds line-of-sight)."),
|
|
129
|
-
status: z.enum(["planned", "active", "paused", "done", "abandoned"]).optional().describe("Lifecycle status (optional; default 'planned')."),
|
|
130
|
-
timeframe: z.string().optional().describe("Free-text timeframe, e.g. 'Q3 2026' or 'H2 2026' (optional)."),
|
|
131
|
-
product_id: z.string().optional().describe("Product to scope it to, from whoami (optional)."),
|
|
132
|
-
}, async (a) => text(await client.createInitiative(a)));
|
|
133
|
-
server.tool("update_initiative", "Update an initiative and return it; omitted fields are unchanged. Re-point it to a different goal with objective_id (null unlinks). status is planned|active|paused|done|abandoned. Resolve the id via list_initiatives; only id is required.", {
|
|
134
|
-
id: z.string().describe("Initiative id, from list_initiatives (required)."),
|
|
135
|
-
name: z.string().optional().describe("New name (optional)."),
|
|
136
|
-
description: z.string().nullable().optional().describe("New description; null clears it (optional)."),
|
|
137
|
-
objective_id: z.string().nullable().optional().describe("New parent goal, from list_objectives; null unlinks (optional)."),
|
|
138
|
-
status: z.enum(["planned", "active", "paused", "done", "abandoned"]).optional().describe("Lifecycle status (optional)."),
|
|
139
|
-
timeframe: z.string().nullable().optional().describe("New timeframe; null clears it (optional)."),
|
|
140
|
-
}, async (a) => text(await client.updateInitiative(a.id, a)));
|
|
141
|
-
server.tool("list_ideas", "List the org's ideas — the native, votable idea backlog — ranked by vote count (highest first). Each returns title, status, vote count, author, and the feature it was promoted to (if any). status is new|under_review|planned|promoted|declined (optional filter). Read-only. Ideas are distinct from insights: an idea is a proposal a team votes on; an insight is customer evidence. Resolve an idea id here before update_idea / vote_idea / promote_idea.", {
|
|
142
|
-
status: z.enum(["new", "under_review", "planned", "promoted", "declined"]).optional().describe("Filter by status (optional)."),
|
|
143
|
-
product_id: z.string().optional().describe("Only ideas for this product, from whoami (optional)."),
|
|
144
|
-
}, async (a) => text(await client.listIdeas({ status: a.status, productId: a.product_id })));
|
|
145
|
-
server.tool("create_idea", "Create an idea in the backlog and return it (starts with 0 votes, status 'new'). Link the evidence it came from with insight_id (from list_insights). product_id defaults to the primary product. Only title is required. Grow it with vote_idea, then promote_idea turns the winner into a roadmap feature.", {
|
|
146
|
-
title: z.string().describe("Idea title (the only required field), e.g. 'Bulk-edit tasks'."),
|
|
147
|
-
body: z.string().optional().describe("The idea in more detail (optional)."),
|
|
148
|
-
insight_id: z.string().optional().describe("Customer insight this idea came from, from list_insights (optional; welds evidence to the idea)."),
|
|
149
|
-
product_id: z.string().optional().describe("Product to scope it to, from whoami (optional)."),
|
|
150
|
-
}, async (a) => text(await client.createIdea(a)));
|
|
151
|
-
server.tool("update_idea", "Update an idea's title / body / status and return it; omitted fields unchanged. status is new|under_review|planned|promoted|declined (set 'promoted' via promote_idea instead, so a feature is actually created). Resolve the id via list_ideas; only id is required.", {
|
|
152
|
-
id: z.string().describe("Idea id, from list_ideas (required)."),
|
|
153
|
-
title: z.string().optional().describe("New title (optional)."),
|
|
154
|
-
body: z.string().nullable().optional().describe("New body; null clears it (optional)."),
|
|
155
|
-
status: z.enum(["new", "under_review", "planned", "promoted", "declined"]).optional().describe("New status (optional; prefer promote_idea over setting 'promoted' by hand)."),
|
|
156
|
-
}, async (a) => text(await client.updateIdea(a.id, a)));
|
|
157
|
-
server.tool("vote_idea", "Cast (or remove) the connected member's vote on an idea and return the new vote state. Adds your vote by default; pass remove:true to take it back. One vote per member — voting twice is a no-op. Resolve the id via list_ideas; only id is required.", {
|
|
158
|
-
id: z.string().describe("Idea id, from list_ideas (required)."),
|
|
159
|
-
remove: z.boolean().optional().describe("true removes your vote instead of adding it (optional; default false)."),
|
|
160
|
-
}, async (a) => text(await client.voteIdea(a.id, a.remove)));
|
|
161
|
-
server.tool("promote_idea", "Promote an idea into a roadmap feature: creates a feature from the idea (name + description), stamps the idea 'promoted' and links it to the new feature, and returns the feature id. Idempotent — an already-promoted idea returns its existing feature. Resolve the id via list_ideas; only id is required. Align the new feature to an initiative/goal afterwards with update_feature.", {
|
|
162
|
-
id: z.string().describe("Idea id to promote, from list_ideas (required)."),
|
|
163
|
-
product_id: z.string().optional().describe("Product to create the feature under, from whoami (optional)."),
|
|
164
|
-
}, async (a) => text(await client.promoteIdea(a.id, a)));
|
|
165
|
-
server.tool("update_sprint", "Update a sprint and return it. state is 'future' | 'active' | 'closed' — moving to 'closed' stamps the completion time, reopening clears it. start_date / end_date are ISO 8601 (or null to clear). Resolve the id via list_sprints; only id is required.", {
|
|
166
|
-
id: z.string().describe("Sprint id, from list_sprints (required)."),
|
|
167
|
-
name: z.string().optional().describe("New name (optional)."),
|
|
168
|
-
goal: z.string().nullable().optional().describe("New goal; null clears it (optional)."),
|
|
169
|
-
state: z.enum(["future", "active", "closed"]).optional().describe("Lifecycle state; 'closed' completes it (optional)."),
|
|
170
|
-
start_date: z.string().nullable().optional().describe("Start, ISO 8601, or null (optional)."),
|
|
171
|
-
end_date: z.string().nullable().optional().describe("End, ISO 8601, or null (optional)."),
|
|
172
|
-
}, async (a) => text(await client.updateSprint(a)));
|
|
173
|
-
server.tool("update_page", "Update a Page — rename, set icon, replace the body, or archive/unarchive (archived:true hides it, false restores it). `body` is plain text (blank lines → paragraphs) and REPLACES the page content. Omitted fields are unchanged. Resolve the id via list_pages; only id is required.", {
|
|
174
|
-
id: z.string().describe("Page id, from list_pages (required)."),
|
|
175
|
-
title: z.string().optional().describe("New title (optional)."),
|
|
176
|
-
icon: z.string().nullable().optional().describe("New emoji icon; null clears it (optional)."),
|
|
177
|
-
body: z.string().optional().describe("New content as plain text; blank lines separate paragraphs. REPLACES existing content (optional)."),
|
|
178
|
-
archived: z.boolean().optional().describe("true archives (hides) the page; false restores it (optional)."),
|
|
179
|
-
}, async (a) => text(await client.updatePage(a)));
|
|
180
|
-
server.tool("create_release", "Create a release and return it (id, version, changelog, released_at). Omit released_at for an unreleased/draft entry. product_id defaults to the org's primary product. Only version is required.", {
|
|
181
|
-
version: z.string().describe("Version string (required), e.g. 'v2.4.0'."),
|
|
182
|
-
changelog: z.string().optional().describe("What shipped (optional)."),
|
|
183
|
-
released_at: z.string().optional().describe("Ship time, ISO 8601 (optional; omit for a draft)."),
|
|
184
|
-
product_id: z.string().optional().describe("Product, from whoami (optional; the primary product when omitted)."),
|
|
185
|
-
}, async (a) => text(await client.createRelease(a)));
|
|
186
|
-
server.tool("update_release", "Update a release and return it; omitted fields unchanged. Set released_at to ship it (or null to move it back to draft). Resolve the id via list_releases; only id is required.", {
|
|
187
|
-
id: z.string().describe("Release id, from list_releases (required)."),
|
|
188
|
-
version: z.string().optional().describe("New version (optional)."),
|
|
189
|
-
changelog: z.string().nullable().optional().describe("New changelog; null clears it (optional)."),
|
|
190
|
-
released_at: z.string().nullable().optional().describe("Ship time ISO 8601, or null for draft (optional)."),
|
|
191
|
-
}, async (a) => text(await client.updateRelease(a)));
|
|
192
|
-
server.tool("create_experiment", "Create a PM experiment (a Build-Measure-Learn hypothesis) and return it. state is 'hypothesis' (default) | 'build' | 'measure' | 'learn'. Only title is required. This is the PM hypothesis tracker list_experiments reads, not the analytics A/B engine.", {
|
|
193
|
-
title: z.string().describe("Experiment title / the hypothesis in a line (required)."),
|
|
194
|
-
hypothesis: z.string().optional().describe("The full hypothesis (optional)."),
|
|
195
|
-
metric: z.string().optional().describe("The metric it moves, e.g. 'activation rate' (optional)."),
|
|
196
|
-
target: z.string().optional().describe("Target change, e.g. '+5pp' (optional)."),
|
|
197
|
-
state: z.enum(["hypothesis", "build", "measure", "learn"]).optional().describe("Build-Measure-Learn stage (optional; default 'hypothesis')."),
|
|
198
|
-
product_id: z.string().optional().describe("Product, from whoami (optional)."),
|
|
199
|
-
}, async (a) => text(await client.createExperiment(a)));
|
|
200
|
-
server.tool("update_experiment", "Update a PM experiment — advance its state and record the outcome — and return it. state ∈ hypothesis|build|measure|learn; verdict ∈ validated|invalidated; decision ∈ pivot|persevere. Resolve the id via list_experiments; only id is required.", {
|
|
201
|
-
id: z.string().describe("Experiment id, from list_experiments (required)."),
|
|
202
|
-
title: z.string().optional().describe("New title (optional)."),
|
|
203
|
-
hypothesis: z.string().nullable().optional().describe("New hypothesis; null clears it (optional)."),
|
|
204
|
-
metric: z.string().nullable().optional().describe("New metric; null clears it (optional)."),
|
|
205
|
-
target: z.string().nullable().optional().describe("New target; null clears it (optional)."),
|
|
206
|
-
state: z.enum(["hypothesis", "build", "measure", "learn"]).optional().describe("Build-Measure-Learn stage (optional)."),
|
|
207
|
-
verdict: z.enum(["validated", "invalidated"]).nullable().optional().describe("Outcome (optional)."),
|
|
208
|
-
decision: z.enum(["pivot", "persevere"]).nullable().optional().describe("What you'll do next (optional)."),
|
|
209
|
-
result: z.string().nullable().optional().describe("Free-text result / what you learned; null clears it (optional)."),
|
|
210
|
-
}, async (a) => text(await client.updateExperiment(a)));
|
|
211
|
-
server.tool("list_decisions", "List the org's logged decisions — title, rationale, status, and any linked feature/release/objective — newest first. Read-only; returns an empty list when none. Optional status filter (decided | proposed | revisit). Resolve a decision id here before update_decision.", {
|
|
212
|
-
status: z.enum(["decided", "proposed", "revisit"]).optional().describe("Filter by status (optional)."),
|
|
213
|
-
}, async (a) => text(await client.listDecisions(a.status)));
|
|
214
|
-
server.tool("create_decision", "Log a decision and return it. status is 'decided' (default) | 'proposed' | 'revisit'; a 'decided' one stamps the decision time. Optionally weld it to a feature / release / objective via link_type + link_id (verified in-org). Only title is required.", {
|
|
215
|
-
title: z.string().describe("The decision in a line (required)."),
|
|
216
|
-
rationale: z.string().optional().describe("Why — the reasoning (optional)."),
|
|
217
|
-
status: z.enum(["decided", "proposed", "revisit"]).optional().describe("Decision status (optional; default 'decided')."),
|
|
218
|
-
link_type: z.enum(["feature", "release", "objective"]).optional().describe("What it's linked to (optional; pair with link_id)."),
|
|
219
|
-
link_id: z.string().optional().describe("Id of the linked feature/release/objective, from list_features / list_releases / list_objectives (optional)."),
|
|
220
|
-
}, async (a) => text(await client.createDecision(a)));
|
|
221
|
-
server.tool("update_decision", "Update a decision and return it; omitted fields unchanged. Moving status to 'decided' re-stamps the decision time. Re-link via link_type + link_id (verified in-org), or clear with nulls. Resolve the id via list_decisions; only id is required.", {
|
|
222
|
-
id: z.string().describe("Decision id, from list_decisions (required)."),
|
|
223
|
-
title: z.string().optional().describe("New title (optional)."),
|
|
224
|
-
rationale: z.string().nullable().optional().describe("New rationale; null clears it (optional)."),
|
|
225
|
-
status: z.enum(["decided", "proposed", "revisit"]).optional().describe("New status (optional)."),
|
|
226
|
-
link_type: z.enum(["feature", "release", "objective"]).nullable().optional().describe("New link target (optional)."),
|
|
227
|
-
link_id: z.string().nullable().optional().describe("New linked id, or null to unlink (optional)."),
|
|
228
|
-
}, async (a) => text(await client.updateDecision(a)));
|
|
229
|
-
server.tool("update_task", "Update one or more of a task's fields and return the updated task; fields you omit are left unchanged (idempotent — re-sending the same values is a no-op), and passing null clears a nullable field. Resolve ids first — the task via list_tasks/get_task, and status/feature/insight/member ids via pm_meta — never guess them. Only id is required.", {
|
|
230
|
-
id: z.string().describe("Task id, from list_tasks or get_task."),
|
|
231
|
-
title: z.string().optional().describe("New title (optional)."),
|
|
232
|
-
description: z.string().nullable().optional().describe("New description; null clears it (optional)."),
|
|
233
|
-
priority: z.enum(["urgent", "high", "normal", "low"]).nullable().optional().describe("New priority; null clears it (optional)."),
|
|
234
|
-
status_id: z.string().nullable().optional().describe("New status id, from pm_meta; null clears it (optional)."),
|
|
235
|
-
feature_id: z.string().nullable().optional().describe("New feature id, from pm_meta; null unlinks (optional)."),
|
|
236
|
-
insight_id: z.string().nullable().optional().describe("New insight id; null unlinks (optional)."),
|
|
237
|
-
assignee_member_ids: z.array(z.string()).optional().describe("Member ids to assign, from pm_meta (optional)."),
|
|
238
|
-
}, async ({ id, ...body }) => text(await client.updateTask(id, body)));
|
|
239
|
-
server.tool("delete_task", "PERMANENTLY delete a task and return the deleted id. Irreversible — there is no undo. Cascades: the task's comments, assignees, tags, attachments, time entries, outcomes, events, relations, and its SUBTASKS are deleted with it; experiment/insight/meeting links to it are cleared. Resolve the id via list_tasks and confirm intent first — prefer update_task (move it to a done/archived status) when you only want it off the active board.", { id: z.string().describe("Task id to permanently delete, from list_tasks (required).") }, async (a) => text(await client.deleteTask(a.id)));
|
|
240
|
-
server.tool("comment_on_task", "Add a comment to a task, authored as the connected member, and return the created comment. Use to record progress, a decision, or a handoff — the comment is visible to the whole org, so keep it work-relevant. Resolve the task id first with list_tasks or get_task; both id and body are required.", {
|
|
241
|
-
id: z.string().describe("Task id, from list_tasks or get_task."),
|
|
242
|
-
body: z.string().describe("Comment body."),
|
|
243
|
-
}, async (a) => text(await client.comment(a.id, a.body)));
|
|
244
|
-
server.tool("get_product_brain", "Read a grounded, read-only snapshot of the org's product so YOU can reason about it on your own model: revenue + top paying accounts, web + product analytics, features, recent verbatim customer signals, and open work. Optional product_id; omit for the primary product. Returns the brain text plus the list of products you can ask about.", { product_id: z.string().optional().describe("Product id, from whoami (optional; primary by default).") }, async (a) => text(await client.brain(a.product_id)));
|
|
245
|
-
server.tool("get_customer_360", "Everything about ONE customer, resolved by id, email, domain, or company name: profile, subscription + MRR, how many users sit under the account, and their verbatim feedback (newest first). Read-only; returns the single best-matching account, or an empty result when nothing matches. The money + people + voice join on one record — use it before answering anything about a specific account.", { query: z.string().describe("The account to resolve — an account id (exact), a user's email (exact), a company domain like 'acme.com', or a company name (partial match). Pass one value; the strongest match wins.") }, async (a) => text(await client.customer360(a.query)));
|
|
246
|
-
server.tool("analyze_nps", "NPS for the product — standard score AND revenue-weighted NPS (each respondent weighted by their account MRR), plus the detractor accounts ranked by what they're worth, with verbatims. The revenue weighting is the spine join no standalone survey tool can compute: it surfaces when your biggest customers are the unhappy ones even if the headline score looks fine. Optional product_id (primary by default) and window_days (default 90).", {
|
|
247
|
-
product_id: z.string().optional().describe("Product id, from whoami (optional; primary by default)."),
|
|
248
|
-
window_days: z.number().optional().describe("Window in days (default 90)."),
|
|
249
|
-
}, async (a) => text(await client.nps({ productId: a.product_id, windowDays: a.window_days })));
|
|
250
|
-
server.tool("analyze_nrr", "Net Revenue Retention — NRR (revenue-weighted) next to logo retention (count-weighted), the expansion/contraction/churn split, and the accounts that lost the most MRR. The divergence is the point: \"you keep 92% of logos but 78% of revenue\" means a big account churned while the headline looks fine. NRR is the number investors ask for and no standalone analytics tool can compute — it needs revenue on the same record. Optional window_days (default 90). Returns status 'building' until enough daily MRR snapshots exist to compare.", { window_days: z.number().optional().describe("Window in days (default 90).") }, async (a) => text(await client.nrr(a.window_days)));
|
|
251
|
-
server.tool("capture_insight", "Write a piece of customer feedback to the spine (the agent's own hand, not just reading) and return the created insight. It fires the same insight.created webhook a manual capture does — a real side-effect, so only capture genuine signal. Tie it to the account it's about (account_id, via get_customer_360) and the feature it concerns (feature_id, via pm_meta) when you know them; kind='opportunity' for a prioritizable ask. Defaults to the primary product; only body is required.", {
|
|
252
|
-
body: z.string().describe("The feedback / insight text."),
|
|
253
|
-
title: z.string().optional().describe("Short title (optional)."),
|
|
254
|
-
kind: z.enum(["insight", "opportunity"]).optional().describe("insight | opportunity (optional)."),
|
|
255
|
-
product_id: z.string().optional().describe("Product id, from whoami (optional; primary by default)."),
|
|
256
|
-
feature_id: z.string().optional().describe("Link to a feature, id from pm_meta (optional)."),
|
|
257
|
-
account_id: z.string().optional().describe("Link to the account it's about, from get_customer_360 (optional)."),
|
|
258
|
-
}, async (a) => text(await client.captureInsight(a)));
|
|
259
|
-
server.tool("analyze_funnel", "Build a conversion funnel from the product's own events and reason over it on your model. Pass `steps` as an ordered list of event names (2+) to compute the funnel: distinct users per step, conversion, and drop-off. Omit `steps` to get the menu of available event names first. Optional product_id (defaults to the primary product) and window_days (default 30).", {
|
|
260
|
-
steps: z.array(z.string()).optional().describe("Ordered event names (2+); omit to get the menu of event names first."),
|
|
261
|
-
product_id: z.string().optional().describe("Product id, from whoami (optional; primary by default)."),
|
|
262
|
-
window_days: z.number().optional().describe("Window in days (default 30)."),
|
|
263
|
-
}, async (a) => text(await client.funnel({ steps: a.steps, productId: a.product_id, window: a.window_days })));
|
|
264
|
-
server.tool("get_retention", "Weekly cohort retention for the product: users grouped by their first-seen week, with the share returning each week after. Read-only; needs product analytics events flowing, and returns empty cohorts when the product has none. Optional product_id (defaults to the primary product) and window_days (default 56 = 8 weekly cohorts).", {
|
|
265
|
-
product_id: z.string().optional().describe("Product id, from whoami (optional; primary by default)."),
|
|
266
|
-
window_days: z.number().optional().describe("Window in days (default 56 = 8 weekly cohorts)."),
|
|
267
|
-
}, async (a) => text(await client.retention({ productId: a.product_id, window: a.window_days })));
|
|
268
|
-
server.tool("analyze_paths", "Trace what users do AFTER a start event — the journey flow (Sankey) from the product's own events, so YOU can reason about real behaviour on your model. Returns nodes (the event at each depth, with distinct users + share of journeys) and links (source→target with how many users took that step), including where people drop off ('(exit)') and the collapsed long tail ('(other)'). Pass `start` to anchor on a specific event, or omit it to anchor on the most common journey start. Optional product_id (defaults to the primary product) and window_days (default 30).", {
|
|
269
|
-
start: z.string().optional().describe("Anchor event name (optional; omit for the most common journey start)."),
|
|
270
|
-
product_id: z.string().optional().describe("Product id, from whoami (optional; primary by default)."),
|
|
271
|
-
window_days: z.number().optional().describe("Window in days (default 30)."),
|
|
272
|
-
}, async (a) => text(await client.paths({ start: a.start, productId: a.product_id, window: a.window_days })));
|
|
273
|
-
server.tool("list_conversations", "List support-chat conversations in the inbox (open + snoozed by default; pass status='all' to include closed). Read-only; returns each with id, visitor, topic, status, and last activity — empty when the inbox is clear. Optional product_id to scope to one product; open a full thread with get_conversation.", {
|
|
274
|
-
product_id: z.string().optional().describe("Scope to one product, id from whoami (optional)."),
|
|
275
|
-
status: z.enum(["all"]).optional().describe("Pass 'all' to include closed conversations (optional)."),
|
|
276
|
-
}, async (a) => text(await client.listConversations({ productId: a.product_id, status: a.status })));
|
|
277
|
-
server.tool("get_conversation", "Read one support conversation: the visitor + the full message thread (visitor, agent, and internal notes), oldest first. Read-only. Resolve the conversation_id first with list_conversations — never guess it; reply with reply_to_conversation or add_note.", { conversation_id: z.string().describe("Conversation id, from list_conversations.") }, async (a) => text(await client.getConversation(a.conversation_id)));
|
|
278
|
-
server.tool("reply_to_conversation", "Send a reply into a support conversation and return the sent message. This message IS VISIBLE TO THE VISITOR in the chat widget — it goes out as you (the member who owns this token), so use add_note instead for internal triage. Resolve the conversation_id first with list_conversations.", {
|
|
279
|
-
conversation_id: z.string().describe("Conversation id, from list_conversations."),
|
|
280
|
-
body: z.string().describe("Reply text the visitor will see."),
|
|
281
|
-
}, async (a) => text(await client.inboxAction({ action: "reply", conversation_id: a.conversation_id, body: a.body })));
|
|
282
|
-
server.tool("add_note", "Add an INTERNAL note to a support conversation and return the created note — visible only in the inbox, never to the visitor. Use to record triage, context, or a handoff for a human. Resolve the conversation_id first with list_conversations.", {
|
|
283
|
-
conversation_id: z.string().describe("Conversation id, from list_conversations."),
|
|
284
|
-
body: z.string().describe("Internal note text (never shown to the visitor)."),
|
|
285
|
-
}, async (a) => text(await client.inboxAction({ action: "note", conversation_id: a.conversation_id, body: a.body })));
|
|
286
|
-
server.tool("resolve_conversation", "Mark a support conversation resolved (status='closed') and return the updated status. Idempotent — resolving an already-closed conversation is a no-op, and a later visitor message reopens it. Resolve the conversation_id first with list_conversations.", { conversation_id: z.string().describe("Conversation id, from list_conversations.") }, async (a) => text(await client.inboxAction({ action: "resolve", conversation_id: a.conversation_id })));
|
|
287
|
-
server.tool("list_bookings", "List scheduled bookings (calls/meetings) — upcoming confirmed ones by default; pass include='all' for past + cancelled. Read-only; returns each with id, event type, guest, host, start/end, and status — empty when nothing is scheduled. Use a booking_id from here with cancel_booking or reschedule_booking.", { include: z.enum(["all"]).optional().describe("Pass 'all' for past + cancelled too (optional).") }, async (a) => text(await client.listBookings({ include: a.include })));
|
|
288
|
-
server.tool("cancel_booking", "Cancel a booking and return the updated booking — frees the slot and cancels the linked meeting, which affects the GUEST, so confirm intent before calling. Cannot cancel a meeting that has already started. Resolve the booking_id first with list_bookings.", { booking_id: z.string().describe("Booking id, from list_bookings.") }, async (a) => text(await client.schedulingAction({ action: "cancel", booking_id: a.booking_id })));
|
|
289
|
-
server.tool("reschedule_booking", "Move a booking to a new start time and return the updated booking — this changes the GUEST's meeting time, so confirm intent before calling. The new time must be a currently-open slot for that event type; cannot reschedule a meeting that has already started. Resolve the booking_id first with list_bookings.", {
|
|
290
|
-
booking_id: z.string().describe("Booking id, from list_bookings."),
|
|
291
|
-
start: z.string().describe("New start time, ISO 8601 (e.g. 2026-06-20T15:00:00Z); must be an open slot."),
|
|
292
|
-
}, async (a) => text(await client.schedulingAction({ action: "reschedule", booking_id: a.booking_id, start: a.start })));
|
|
293
|
-
server.tool("list_channels", "List the team Comms channels you (this token's member) belong to — id, name, kind, topic. Read-only; returns your channels, empty when you belong to none (an admin adds you in AIOProductOS → Comms). These are the channels you can read_channel and post_to_channel into.", {}, async () => text(await client.listChannels()));
|
|
294
|
-
server.tool("read_channel", "Read recent messages in a Comms channel you belong to, oldest→newest, so you can catch up before replying. Read-only; returns each message with its author, body, and timestamp — empty when the channel is silent. Resolve the channel_id first with list_channels — never guess it. Pass limit to cap how many of the most-recent messages come back; the server applies a default when it's omitted.", {
|
|
295
|
-
channel_id: z.string().describe("Channel id, from list_channels."),
|
|
296
|
-
limit: z.number().int().positive().optional().describe("Maximum number of most-recent messages to return (optional; the server applies a sensible default when omitted)."),
|
|
297
|
-
}, async (a) => text(await client.readChannel(a.channel_id, a.limit)));
|
|
298
|
-
server.tool("post_to_channel", "Post a message into a team Comms channel you belong to and return the posted message. It goes out as you (this token's member) and appears live for your teammates — use it to tell the team what you did, share a link, or ask a question. Resolve the channel_id first with list_channels; only works for channels you're a member of.", {
|
|
299
|
-
channel_id: z.string().describe("Channel id, from list_channels."),
|
|
300
|
-
body: z.string().describe("Message text, visible to all channel members."),
|
|
301
|
-
}, async (a) => text(await client.postToChannel(a.channel_id, a.body)));
|
|
302
|
-
server.tool("reply_in_channel", "Reply in a thread under a specific message in a Comms channel you belong to, and return the posted reply — it goes out as you, visible to the channel. Resolve channel_id via list_channels and the parent message's id via read_channel.", {
|
|
303
|
-
channel_id: z.string().describe("Channel id, from list_channels."),
|
|
304
|
-
parent_id: z.string().describe("Parent message id, from read_channel."),
|
|
305
|
-
body: z.string().describe("Reply text, visible to all channel members."),
|
|
306
|
-
}, async (a) => text(await client.replyInChannel(a.channel_id, a.parent_id, a.body)));
|
|
307
|
-
// ── Read parity with the hosted connector ───────────────────────────────────
|
|
308
|
-
// The full catalogue + strategy ladder + docs + identity + artifact reads, so
|
|
309
|
-
// the stdio agent grounds on exactly what the hosted connector sees.
|
|
310
|
-
server.tool("list_features", "The product's feature catalogue with description, status, and when each was last touched — richer than pm_meta (id+name only). Read-only; empty when none. Optional product_id (from whoami) and free-text q over name+key. Use a feature id from here to link a task or insight on the spine.", {
|
|
311
|
-
q: z.string().optional().describe("Free-text search over feature name + key (optional)."),
|
|
312
|
-
product_id: z.string().optional().describe("Product id to scope to, from whoami (optional; spans all products when omitted)."),
|
|
313
|
-
}, async (a) => text(await client.listFeatures({ q: a.q, productId: a.product_id })));
|
|
314
|
-
server.tool("list_objectives", "List the org's OKRs — each objective with its key results and live progress (0..1 between start and target). Read-only; empty when none set. Read it before prioritising; tie proposed tasks to the objective they move and cite live progress when arguing priority. Optional product_id, from whoami.", { product_id: z.string().optional().describe("Product id to scope to, from whoami (optional; spans all products when omitted).") }, async (a) => text(await client.listObjectives(a.product_id)));
|
|
315
|
-
server.tool("list_experiments", "List the org's experiments with their state and linked feature. Read-only; empty when none. Optional product_id (from whoami) and state filter. Use an experiment id from here with review_artifact or list_artifact_versions.", {
|
|
316
|
-
product_id: z.string().optional().describe("Product id to scope to, from whoami (optional; spans all products when omitted)."),
|
|
317
|
-
state: z.string().optional().describe("Only experiments in this state (optional)."),
|
|
318
|
-
}, async (a) => text(await client.listExperiments({ productId: a.product_id, state: a.state })));
|
|
319
|
-
server.tool("list_releases", "List the org's releases (version, date, linked features). Read-only; empty when none. Optional product_id, from whoami. Use it to see what shipped and when, then open slipped work with list_features or get_roadmap_drift.", { product_id: z.string().optional().describe("Product id to scope to, from whoami (optional; spans all products when omitted).") }, async (a) => text(await client.listReleases(a.product_id)));
|
|
320
|
-
server.tool("list_sprints", "List the org's sprints with their state and dates. Read-only; empty when none. Optional state filter (e.g. active | planned | closed). Resolve a sprint id here before scheduling tasks into it via update_task / update_sprint.", { state: z.string().optional().describe("Only sprints in this state, e.g. active | planned | closed (optional).") }, async (a) => text(await client.listSprints(a.state)));
|
|
321
|
-
server.tool("list_pages", "List the org's Pages (docs / PRDs) with title and last-updated — id+title for resolution. Read-only; empty when none. Optional product_id, from whoami. Get one page's full content with get_page.", { product_id: z.string().optional().describe("Product id to scope to, from whoami (optional; spans all products when omitted).") }, async (a) => text(await client.listPages(a.product_id)));
|
|
322
|
-
server.tool("get_page", "Read one Page (doc / PRD) by id and return its full content. Read-only. Resolve the id first with list_pages — never guess it.", { id: z.string().describe("Page id, from list_pages.") }, async (a) => text(await client.getPage(a.id)));
|
|
323
|
-
server.tool("list_insights", "Search the captured insight backlog (voice of customer) — the read twin of capture_insight. Read-only; matching insights newest first, empty when nothing matches. Filters: status, kind (insight|opportunity), feature_id, account_id, product_id, free-text q over title+body; limit default 50, max 200. Survey the evidence behind a feature or account before prioritising.", {
|
|
324
|
-
q: z.string().optional().describe("Free-text search over title + body (optional)."),
|
|
325
|
-
status: z.string().optional().describe("Only insights in this workflow status (optional)."),
|
|
326
|
-
kind: z.enum(["insight", "opportunity"]).optional().describe("'insight' = raw signal; 'opportunity' = a prioritisable ask (optional)."),
|
|
327
|
-
feature_id: z.string().optional().describe("Only insights linked to this feature; id via list_features or pm_meta (optional)."),
|
|
328
|
-
account_id: z.string().optional().describe("Only insights about this account; id via get_customer_360 (optional)."),
|
|
329
|
-
product_id: z.string().optional().describe("Product id to scope to, from whoami (optional; spans all products when omitted)."),
|
|
330
|
-
limit: z.number().int().min(1).max(200).optional().describe("Max rows to return (optional; default 50, max 200)."),
|
|
331
|
-
}, async (a) => text(await client.listInsights({
|
|
332
|
-
q: a.q,
|
|
333
|
-
status: a.status,
|
|
334
|
-
kind: a.kind,
|
|
335
|
-
featureId: a.feature_id,
|
|
336
|
-
accountId: a.account_id,
|
|
337
|
-
productId: a.product_id,
|
|
338
|
-
limit: a.limit,
|
|
339
|
-
})));
|
|
340
|
-
server.tool("get_roadmap_drift", "Planned vs shipped features over a window: a drift score (0-100, 100 = perfect alignment), counts (planned / shipped / on-time / slipped / unplanned / orphaned), median slip days, and the top slipped + unplanned ships. Deterministic, no LLM cost. window = week | month | quarter (default quarter); optional product_id. Read-only; zeroed when nothing was planned or shipped. Use it in planning reviews, then open slipped features with list_features.", {
|
|
341
|
-
window: z.enum(["week", "month", "quarter"]).optional().describe("Lookback window to compare planned vs shipped (optional; default quarter)."),
|
|
342
|
-
product_id: z.string().optional().describe("Product id, from whoami (optional; spans all the org's products when omitted)."),
|
|
343
|
-
}, async (a) => text(await client.roadmapDrift({ window: a.window, productId: a.product_id })));
|
|
344
|
-
server.tool("get_weekly_signal_memo", "The deterministic weekly signal memo — what moved on the spine this week (revenue, top accounts, fresh verbatim signal, shipped + slipped work). Read-only. Optional week (ISO week, e.g. 2026-W29; defaults to the current week); set generate=true to (re)compute it if this week's memo isn't cached yet.", {
|
|
345
|
-
week: z.string().optional().describe("ISO week, e.g. '2026-W29' (optional; defaults to the current week)."),
|
|
346
|
-
generate: z.boolean().optional().describe("Recompute the memo if it isn't cached yet (optional)."),
|
|
347
|
-
}, async (a) => text(await client.weeklySignalMemo({ week: a.week, generate: a.generate })));
|
|
348
|
-
server.tool("get_codebase_map", "The product's codebase map — modules and how they connect, with live spine pulses (recent deploys / activity). Read-only. Optional product_id, from whoami. Use it to ground engineering-adjacent questions in the actual code structure.", { product_id: z.string().optional().describe("Product id to scope to, from whoami (optional; spans all products when omitted).") }, async (a) => text(await client.codebaseMap(a.product_id)));
|
|
349
|
-
server.tool("list_artifact_versions", "List the immutable version history of one AI artifact (a feature spec, experiment plan, or page) — each version with its score and when it was written. Read-only. Resolve target_id via list_features / list_experiments / list_pages first. Pair with revert_to_version to roll one back, or review_artifact to add a new reviewed version.", {
|
|
350
|
-
target_id: z.string().optional().describe("Id of the feature / experiment / page whose versions to list, from list_features / list_experiments / list_pages."),
|
|
351
|
-
target_type: z.enum(["feature", "experiment", "page"]).optional().describe("What kind of artifact target_id is (optional)."),
|
|
352
|
-
}, async (a) => text(await client.listArtifactVersions({ targetId: a.target_id, targetType: a.target_type })));
|
|
353
|
-
server.tool("get_device_candidates", "Clusters of ≥2 end-users seen on the same device: 'anon_bridge' (high confidence — an anonymous visitor later identified) or 'device_shared' (low confidence — review only). Read-only; empty when none found. Use it to find merge targets, then act with merge_end_users.", {}, async () => text(await client.deviceCandidates()));
|
|
354
|
-
server.tool("list_identity_merges", "List the org's end-user merge history, newest first — each event with its id, kind (merge | unmerge), target + source ids, reason, who ran it, when, and whether a merge was already reverted. Read-only; empty when none have run. Use it to audit identity changes and find the event id for unmerge_end_users (only un-reverted merges can be undone).", { limit: z.number().int().min(1).max(200).optional().describe("Max events to return (optional; default 50, max 200).") }, async (a) => text(await client.listIdentityMerges(a.limit)));
|
|
355
|
-
server.tool("merge_end_users", "Merge source end-users into a target and return the result, including the merge event id (also in list_identity_merges): all FK rows (events, insights, tasks, …) re-point onto the target and the sources are tombstoned. A write; reversible for 30 days via unmerge_end_users. Get the candidate ids from get_device_candidates first — never guess which users to fold together.", {
|
|
356
|
-
target_end_user_id: z.string().describe("UUID of the end-user to keep, from get_device_candidates."),
|
|
357
|
-
source_end_user_ids: z.array(z.string()).describe("UUIDs of end-users to fold into the target, from get_device_candidates."),
|
|
358
|
-
reason: z.string().optional().describe("Why the merge (optional, recorded)."),
|
|
359
|
-
}, async (a) => text(await client.mergeEndUsers({ target_end_user_id: a.target_end_user_id, source_end_user_ids: a.source_end_user_ids, reason: a.reason })));
|
|
360
|
-
server.tool("unmerge_end_users", "Undo a previous end-user merge by its event id and return the result — the tombstoned sources are restored and rows re-split. A write; only an un-reverted merge event can be undone. Find the event id with list_identity_merges — never guess it.", { event_id: z.string().describe("Merge event id to undo, from list_identity_merges.") }, async (a) => text(await client.unmergeEndUsers(a.event_id)));
|
|
361
|
-
server.tool("review_artifact", "Agent-as-critic over a DRAFT artifact (a feature spec, experiment plan, or page): checks it against a baseline PM bar — clear problem/hypothesis, a measurable success metric, evidence cited, risks named, a rollout/experiment plan — and returns structured findings (section, severity, concrete fix, verbatim evidence quote) plus a 0-100 score. A write: each call re-runs the review and persists a new version (see list_artifact_versions). Resolve target_id first via list_features / list_experiments / list_pages. One small LLM call; use it before sending a draft for sign-off.", {
|
|
362
|
-
target_id: z.string().describe("Id of the feature / experiment / page to review, from list_features / list_experiments / list_pages."),
|
|
363
|
-
target_type: z.enum(["feature", "experiment", "page"]).describe("What kind of artifact target_id is: a feature (spec), experiment (plan), or page (doc/PRD)."),
|
|
364
|
-
rubric_id: z.string().optional().describe("Score against a specific rubric; omit to use the org's default (or the built-in baseline)."),
|
|
365
|
-
}, async (a) => text(await client.reviewArtifact({ target_id: a.target_id, target_type: a.target_type, rubric_id: a.rubric_id })));
|
|
366
|
-
server.tool("revert_to_version", "Roll an AI artifact back to a prior version by version_id and return the result — the artifact's live content is replaced with that version's, recorded as a new version so nothing is lost. A write. Find the version_id with list_artifact_versions — never guess it.", {
|
|
367
|
-
version_id: z.string().describe("Version id to revert to, from list_artifact_versions."),
|
|
368
|
-
reason: z.string().optional().describe("Why the revert (optional, recorded)."),
|
|
369
|
-
}, async (a) => text(await client.revertToVersion({ version_id: a.version_id, reason: a.reason })));
|
|
370
|
-
// ── Routines (slash-command prompts) ────────────────────────────────────────
|
|
371
|
-
// Surface in the AI host as named slash commands (e.g. Claude Code:
|
|
372
|
-
// /productos:standup), so the human — or a scheduled headless run — can run a
|
|
373
|
-
// product routine on demand. Each orchestrates the tools above; they stay
|
|
374
|
-
// read-first and only write what the playbook sanctions.
|
|
375
|
-
server.prompt("standup", "Board standup — what moved, what's blocked, what needs you today (read-only).", async () => promptMsg(`Run the product standup for the connected org. Read-only — don't change anything.\n` +
|
|
376
|
-
`1) pm_meta to resolve lists / statuses / members.\n` +
|
|
377
|
-
`2) list_tasks to read the active board.\n` +
|
|
378
|
-
`Report briefly: what's in progress, what's blocked (and why), what's gone stale with no movement, and the 1–3 ` +
|
|
379
|
-
`decisions that need a human today. Pull get_product_brain if revenue/account context sharpens the call. ` +
|
|
380
|
-
`End with the single most important thing to do next.`));
|
|
381
|
-
server.prompt("triage", "Triage — turn fresh customer signal into prioritized, spine-linked work.", async () => promptMsg(`Run intake triage for the connected org. Follow get_pm_playbook.\n` +
|
|
382
|
-
`1) get_product_brain — ground in revenue, top accounts, recent verbatim signal, and open work.\n` +
|
|
383
|
-
`2) Review the board (list_tasks, pm_meta) and any fresh insights.\n` +
|
|
384
|
-
`3) Prioritize on EVIDENCE — affected accounts + MRR + reach — never an invented score.\n` +
|
|
385
|
-
`4) Make the writes you're confident in: create_task / update_task, linking insight→feature→task; ` +
|
|
386
|
-
`assign owners; place the clear ones in the current sprint. SURFACE the judgment calls for a human instead of guessing.\n` +
|
|
387
|
-
`Confirm only the writes you actually made.`));
|
|
388
|
-
server.prompt("daily", "Daily PM briefing — your plate, blockers, and the one thing to do next.", async () => promptMsg(`Give the connected member their daily briefing.\n` +
|
|
389
|
-
`1) get_product_brain for context.\n` +
|
|
390
|
-
`2) list_tasks + pm_meta — what's assigned to them, what's blocked, what's overdue or stale.\n` +
|
|
391
|
-
`3) Scan the support inbox (list_conversations) and upcoming calls (list_bookings) for anything urgent.\n` +
|
|
392
|
-
`Output a tight briefing: top priorities today, blockers needing a decision, and ONE recommended next action. ` +
|
|
393
|
-
`Read-only unless they ask you to act.`));
|
|
394
34
|
async function main() {
|
|
395
|
-
// Fire-and-forget: if a newer version is on npm, arm the one-time banner
|
|
396
|
-
//
|
|
35
|
+
// Fire-and-forget: if a newer version is on npm, arm the one-time banner.
|
|
36
|
+
// Never blocks the transport, never throws.
|
|
397
37
|
void checkForUpdate(VERSION)
|
|
398
38
|
.then((latest) => {
|
|
399
39
|
if (latest)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface RestCall {
|
|
2
|
+
method: "GET" | "POST" | "PATCH" | "DELETE";
|
|
3
|
+
/** Path + query, relative to the platform origin (e.g. "/api/me/nps?window_days=90"). */
|
|
4
|
+
path: string;
|
|
5
|
+
body?: unknown;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* MCP tool behaviour hints (spec 2025-06-18). They're advisory — a client uses
|
|
9
|
+
* them to decide what to auto-run vs. confirm — and they're a trust signal for
|
|
10
|
+
* the Connectors Directory: every tool here declares whether it mutates and
|
|
11
|
+
* whether it can destroy. NONE of our tools are destructive (no deletes).
|
|
12
|
+
*/
|
|
13
|
+
export interface ToolAnnotations {
|
|
14
|
+
/** Tool only reads — never writes. */
|
|
15
|
+
readOnlyHint: boolean;
|
|
16
|
+
/** Tool may perform irreversible/destructive updates. Always false here. */
|
|
17
|
+
destructiveHint: boolean;
|
|
18
|
+
/** Repeating the call with the same args has no additional effect. */
|
|
19
|
+
idempotentHint: boolean;
|
|
20
|
+
/** Tool reaches an open/external world (e.g. web search). False — closed to the caller's own org. */
|
|
21
|
+
openWorldHint: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A worked example for a tool — the natural-language intent a user would express
|
|
25
|
+
* and the arguments the model would pass. Surfaced two ways in tools/list: as a
|
|
26
|
+
* tool-level `examples` array (rich connector pickers render it) and, for the
|
|
27
|
+
* arguments, as JSON-Schema `inputSchema.examples` (the standard, portable spot).
|
|
28
|
+
*/
|
|
29
|
+
export interface ToolExample {
|
|
30
|
+
/** What the user is trying to do — the prompt that triggers this call. */
|
|
31
|
+
description: string;
|
|
32
|
+
/** The arguments to pass. `{}` for a no-argument tool. */
|
|
33
|
+
arguments: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
export type McpTool = {
|
|
36
|
+
name: string;
|
|
37
|
+
/** Human-readable display title for connector pickers (MCP spec 2025-06-18). */
|
|
38
|
+
title?: string;
|
|
39
|
+
description: string;
|
|
40
|
+
/** JSON Schema for the tool's arguments (MCP tools/list contract). */
|
|
41
|
+
inputSchema: Record<string, unknown>;
|
|
42
|
+
/** Behaviour hints. Omitted → READ_ONLY default (see annotationsFor). */
|
|
43
|
+
annotations?: ToolAnnotations;
|
|
44
|
+
/** Worked usage examples (see ToolExample). */
|
|
45
|
+
examples?: ToolExample[];
|
|
46
|
+
/**
|
|
47
|
+
* #65 — optional rich view. Given the tool's JSON response text, return a
|
|
48
|
+
* self-contained HTML card (or null). The dispatch embeds it as a `ui://`
|
|
49
|
+
* resource AFTER the text block: MCP-UI hosts render it, others ignore it —
|
|
50
|
+
* progressive enhancement, never a facade (the text is always present).
|
|
51
|
+
*/
|
|
52
|
+
htmlView?: (raw: string) => string | null;
|
|
53
|
+
} & ({
|
|
54
|
+
kind: "rest";
|
|
55
|
+
rest: (a: Record<string, unknown>) => RestCall;
|
|
56
|
+
} | {
|
|
57
|
+
kind: "static";
|
|
58
|
+
run: (a: Record<string, unknown>) => string;
|
|
59
|
+
});
|
|
60
|
+
/** Every tool surfaces hints in tools/list; reads inherit the READ_ONLY default. */
|
|
61
|
+
export declare function annotationsFor(t: McpTool): ToolAnnotations;
|
|
62
|
+
export declare const MCP_TOOLS: McpTool[];
|
|
63
|
+
export declare const MCP_TOOLS_BY_NAME: Record<string, McpTool>;
|