@aioproductoscom/mcp 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/LICENSE +1 -1
  2. package/dist/index.js +117 -54
  3. package/package.json +19 -7
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 AIITDEVELOPMENT DOO Novi Sad
3
+ Copyright (c) 2026 AIOProductOS Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/dist/index.js CHANGED
@@ -18,7 +18,12 @@ function text(data) {
18
18
  ],
19
19
  };
20
20
  }
21
- const server = new McpServer({ name: "productos", version: "0.13.0" }, {
21
+ /** A prompt result one user-role message the host injects when the slash
22
+ * command runs. Surfaces as a named slash command in the AI host. */
23
+ function promptMsg(body) {
24
+ return { messages: [{ role: "user", content: { type: "text", text: body } }] };
25
+ }
26
+ const server = new McpServer({ name: "AIOProductOS", version: "0.15.0" }, {
22
27
  instructions: "You manage product work on ProductOS for the connected member — board, insights, features, and " +
23
28
  "analytics all hang off one spine. Call get_pm_playbook first for how to operate: ground in " +
24
29
  "get_product_brain, keep work tied to the spine (insight→feature→task→outcome), and prioritize on " +
@@ -26,67 +31,125 @@ const server = new McpServer({ name: "productos", version: "0.13.0" }, {
26
31
  "pm_meta before create_task / update_task; never guess an id. Confirm what you changed in plain " +
27
32
  "language, and claim only writes you actually made.",
28
33
  });
29
- server.tool("whoami", "Show the connected ProductOS identity (org, member).", {}, async () => text(await client.whoami()));
34
+ server.tool("whoami", "Show the connected ProductOS 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()));
30
35
  server.tool("get_pm_playbook", "How to operate as a product manager on ProductOS — 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}`));
31
- server.tool("pm_meta", "List the org's PM lists, statuses, members, and features use to resolve names to ids before create/update.", {}, async () => text(await client.meta()));
32
- server.tool("list_tasks", "List tasks in the org. Optional filters: status_id, list_id.", { status_id: z.string().optional(), list_id: z.string().optional() }, async (a) => text(await client.listTasks(a)));
33
- server.tool("get_task", "Get one task with its comments and assignees.", { id: z.string() }, async (a) => text(await client.getTask(a.id)));
34
- server.tool("create_task", "Create a task. Omitting list_id uses the org's first list. feature_id / insight_id link it to the ProductOS spine.", {
35
- title: z.string(),
36
- description: z.string().optional(),
37
- priority: z.enum(["urgent", "high", "normal", "low"]).optional(),
38
- list_id: z.string().optional(),
39
- status_id: z.string().optional(),
40
- feature_id: z.string().optional(),
41
- insight_id: z.string().optional(),
42
- assignee_member_ids: z.array(z.string()).optional(),
36
+ 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()));
37
+ 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.", {
38
+ status_id: z.string().optional().describe("Filter by status id, from pm_meta."),
39
+ list_id: z.string().optional().describe("Filter by list id, from pm_meta."),
40
+ }, async (a) => text(await client.listTasks(a)));
41
+ 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)));
42
+ 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 ProductOS spine. Resolve list/status/feature/insight/member ids via pm_meta first — never guess them. Only title is required.", {
43
+ title: z.string().describe("Task title."),
44
+ description: z.string().optional().describe("Task description (optional)."),
45
+ priority: z.enum(["urgent", "high", "normal", "low"]).optional().describe("Priority (optional)."),
46
+ list_id: z.string().optional().describe("List id, from pm_meta (optional; defaults to the org's first list)."),
47
+ status_id: z.string().optional().describe("Status id, from pm_meta (optional)."),
48
+ feature_id: z.string().optional().describe("Link to a feature, id from pm_meta (optional)."),
49
+ insight_id: z.string().optional().describe("Link to an insight (optional)."),
50
+ assignee_member_ids: z.array(z.string()).optional().describe("Member ids to assign, from pm_meta (optional)."),
43
51
  }, async (a) => text(await client.createTask(a)));
44
- server.tool("update_task", "Update a task's fields (status, priority, title, description, feature, insight, assignees).", {
45
- id: z.string(),
46
- title: z.string().optional(),
47
- description: z.string().nullable().optional(),
48
- priority: z.enum(["urgent", "high", "normal", "low"]).nullable().optional(),
49
- status_id: z.string().nullable().optional(),
50
- feature_id: z.string().nullable().optional(),
51
- insight_id: z.string().nullable().optional(),
52
- assignee_member_ids: z.array(z.string()).optional(),
52
+ 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.", {
53
+ id: z.string().describe("Task id, from list_tasks or get_task."),
54
+ title: z.string().optional().describe("New title (optional)."),
55
+ description: z.string().nullable().optional().describe("New description; null clears it (optional)."),
56
+ priority: z.enum(["urgent", "high", "normal", "low"]).nullable().optional().describe("New priority; null clears it (optional)."),
57
+ status_id: z.string().nullable().optional().describe("New status id, from pm_meta; null clears it (optional)."),
58
+ feature_id: z.string().nullable().optional().describe("New feature id, from pm_meta; null unlinks (optional)."),
59
+ insight_id: z.string().nullable().optional().describe("New insight id; null unlinks (optional)."),
60
+ assignee_member_ids: z.array(z.string()).optional().describe("Member ids to assign, from pm_meta (optional)."),
53
61
  }, async ({ id, ...body }) => text(await client.updateTask(id, body)));
54
- server.tool("comment_on_task", "Add a comment to a task.", { id: z.string(), body: z.string() }, async (a) => text(await client.comment(a.id, a.body)));
55
- 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() }, async (a) => text(await client.brain(a.product_id)));
56
- 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. The money + people + voice join on one record — use it before answering anything about a specific account.", { query: z.string() }, async (a) => text(await client.customer360(a.query)));
57
- 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).", { product_id: z.string().optional(), window_days: z.number().optional() }, async (a) => text(await client.nps({ productId: a.product_id, windowDays: a.window_days })));
58
- 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() }, async (a) => text(await client.nrr(a.window_days)));
59
- server.tool("capture_insight", "Write a piece of customer feedback to the spine (the agent's own hand, not just reading). It fires the same insight.created webhook a manual capture does. Tie it to the account it's about (account_id) and the feature it concerns (feature_id) when you know them; kind='opportunity' for a prioritizable ask. Defaults to the primary product.", {
60
- body: z.string(),
61
- title: z.string().optional(),
62
- kind: z.enum(["insight", "opportunity"]).optional(),
63
- product_id: z.string().optional(),
64
- feature_id: z.string().optional(),
65
- account_id: z.string().optional(),
62
+ 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.", {
63
+ id: z.string().describe("Task id, from list_tasks or get_task."),
64
+ body: z.string().describe("Comment body."),
65
+ }, async (a) => text(await client.comment(a.id, a.body)));
66
+ 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)));
67
+ 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. Read-only; returns the matched 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("Account id, email, domain, or company name.") }, async (a) => text(await client.customer360(a.query)));
68
+ 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).", {
69
+ product_id: z.string().optional().describe("Product id, from whoami (optional; primary by default)."),
70
+ window_days: z.number().optional().describe("Window in days (default 90)."),
71
+ }, async (a) => text(await client.nps({ productId: a.product_id, windowDays: a.window_days })));
72
+ 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)));
73
+ 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.", {
74
+ body: z.string().describe("The feedback / insight text."),
75
+ title: z.string().optional().describe("Short title (optional)."),
76
+ kind: z.enum(["insight", "opportunity"]).optional().describe("insight | opportunity (optional)."),
77
+ product_id: z.string().optional().describe("Product id, from whoami (optional; primary by default)."),
78
+ feature_id: z.string().optional().describe("Link to a feature, id from pm_meta (optional)."),
79
+ account_id: z.string().optional().describe("Link to the account it's about, from get_customer_360 (optional)."),
66
80
  }, async (a) => text(await client.captureInsight(a)));
67
81
  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).", {
68
- steps: z.array(z.string()).optional(),
69
- product_id: z.string().optional(),
70
- window_days: z.number().optional(),
82
+ steps: z.array(z.string()).optional().describe("Ordered event names (2+); omit to get the menu of event names first."),
83
+ product_id: z.string().optional().describe("Product id, from whoami (optional; primary by default)."),
84
+ window_days: z.number().optional().describe("Window in days (default 30)."),
71
85
  }, async (a) => text(await client.funnel({ steps: a.steps, productId: a.product_id, window: a.window_days })));
72
- server.tool("get_retention", "Weekly cohort retention for the product: users grouped by their first-seen week, with the share returning each week after. Optional product_id (defaults to the primary product) and window_days (default 56 = 8 weekly cohorts).", { product_id: z.string().optional(), window_days: z.number().optional() }, async (a) => text(await client.retention({ productId: a.product_id, window: a.window_days })));
86
+ 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).", {
87
+ product_id: z.string().optional().describe("Product id, from whoami (optional; primary by default)."),
88
+ window_days: z.number().optional().describe("Window in days (default 56 = 8 weekly cohorts)."),
89
+ }, async (a) => text(await client.retention({ productId: a.product_id, window: a.window_days })));
73
90
  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).", {
74
- start: z.string().optional(),
75
- product_id: z.string().optional(),
76
- window_days: z.number().optional(),
91
+ start: z.string().optional().describe("Anchor event name (optional; omit for the most common journey start)."),
92
+ product_id: z.string().optional().describe("Product id, from whoami (optional; primary by default)."),
93
+ window_days: z.number().optional().describe("Window in days (default 30)."),
77
94
  }, async (a) => text(await client.paths({ start: a.start, productId: a.product_id, window: a.window_days })));
78
- server.tool("list_conversations", "List support-chat conversations in the inbox (open + snoozed by default; pass status='all' to include closed). Each has id, visitor, topic, status, and last activity. Optional product_id to scope to one product.", { product_id: z.string().optional(), status: z.enum(["all"]).optional() }, async (a) => text(await client.listConversations({ productId: a.product_id, status: a.status })));
79
- server.tool("get_conversation", "Read one support conversation: the visitor + the full message thread (visitor, agent, and internal notes), oldest first.", { conversation_id: z.string() }, async (a) => text(await client.getConversation(a.conversation_id)));
80
- server.tool("reply_to_conversation", "Send a reply into a support conversation. This message IS VISIBLE TO THE VISITOR in the chat widget — it goes out as you (the member who owns this token). Use add_note for internal triage you don't want the visitor to see.", { conversation_id: z.string(), body: z.string() }, async (a) => text(await client.inboxAction({ action: "reply", conversation_id: a.conversation_id, body: a.body })));
81
- server.tool("add_note", "Add an INTERNAL note to a support conversation — visible only in the inbox, never to the visitor. Use to record triage, context, or a handoff for a human.", { conversation_id: z.string(), body: z.string() }, async (a) => text(await client.inboxAction({ action: "note", conversation_id: a.conversation_id, body: a.body })));
82
- server.tool("resolve_conversation", "Mark a support conversation resolved (status='closed'). A later visitor message reopens it.", { conversation_id: z.string() }, async (a) => text(await client.inboxAction({ action: "resolve", conversation_id: a.conversation_id })));
83
- server.tool("list_bookings", "List scheduled bookings (calls/meetings) upcoming confirmed ones by default; pass include='all' for past + cancelled. Each has id, event type, guest, host, start/end, and status.", { include: z.enum(["all"]).optional() }, async (a) => text(await client.listBookings({ include: a.include })));
84
- server.tool("cancel_booking", "Cancel a booking by id — frees the slot and cancels the linked meeting. Cannot cancel a meeting that has already started.", { booking_id: z.string() }, async (a) => text(await client.schedulingAction({ action: "cancel", booking_id: a.booking_id })));
85
- server.tool("reschedule_booking", "Move a booking to a new start time (ISO 8601, e.g. 2026-06-20T15:00:00Z). The new time must be a currently-open slot for that event type. Cannot reschedule a meeting that has already started.", { booking_id: z.string(), start: z.string() }, async (a) => text(await client.schedulingAction({ action: "reschedule", booking_id: a.booking_id, start: a.start })));
86
- server.tool("list_channels", "List the team Comms channels you (this token's member) belong to id, name, kind, topic. These are the channels you can read and post into. An admin adds you to a channel in ProductOS → Comms.", {}, async () => text(await client.listChannels()));
87
- server.tool("read_channel", "Read recent messages in a Comms channel you belong to (oldest→newest), so you can catch up before replying. Pass the channel_id from list_channels.", { channel_id: z.string(), limit: z.number().optional() }, async (a) => text(await client.readChannel(a.channel_id, a.limit)));
88
- server.tool("post_to_channel", "Post a message into a team Comms channel you belong to. 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. Only works for channels you're a member of (see list_channels).", { channel_id: z.string(), body: z.string() }, async (a) => text(await client.postToChannel(a.channel_id, a.body)));
89
- server.tool("reply_in_channel", "Reply in a thread under a specific message in a Comms channel you belong to. Pass the parent message's id (from read_channel) as parent_id.", { channel_id: z.string(), parent_id: z.string(), body: z.string() }, async (a) => text(await client.replyInChannel(a.channel_id, a.parent_id, a.body)));
95
+ 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.", {
96
+ product_id: z.string().optional().describe("Scope to one product, id from whoami (optional)."),
97
+ status: z.enum(["all"]).optional().describe("Pass 'all' to include closed conversations (optional)."),
98
+ }, async (a) => text(await client.listConversations({ productId: a.product_id, status: a.status })));
99
+ 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)));
100
+ 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.", {
101
+ conversation_id: z.string().describe("Conversation id, from list_conversations."),
102
+ body: z.string().describe("Reply text the visitor will see."),
103
+ }, async (a) => text(await client.inboxAction({ action: "reply", conversation_id: a.conversation_id, body: a.body })));
104
+ 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.", {
105
+ conversation_id: z.string().describe("Conversation id, from list_conversations."),
106
+ body: z.string().describe("Internal note text (never shown to the visitor)."),
107
+ }, async (a) => text(await client.inboxAction({ action: "note", conversation_id: a.conversation_id, body: a.body })));
108
+ 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 })));
109
+ 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 })));
110
+ 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 })));
111
+ 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.", {
112
+ booking_id: z.string().describe("Booking id, from list_bookings."),
113
+ start: z.string().describe("New start time, ISO 8601 (e.g. 2026-06-20T15:00:00Z); must be an open slot."),
114
+ }, async (a) => text(await client.schedulingAction({ action: "reschedule", booking_id: a.booking_id, start: a.start })));
115
+ 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 ProductOS → Comms). These are the channels you can read_channel and post_to_channel into.", {}, async () => text(await client.listChannels()));
116
+ 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 the messages, empty when the channel is silent. Resolve the channel_id first with list_channels — never guess it.", {
117
+ channel_id: z.string().describe("Channel id, from list_channels."),
118
+ limit: z.number().optional().describe("Max messages to return (optional)."),
119
+ }, async (a) => text(await client.readChannel(a.channel_id, a.limit)));
120
+ 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.", {
121
+ channel_id: z.string().describe("Channel id, from list_channels."),
122
+ body: z.string().describe("Message text, visible to all channel members."),
123
+ }, async (a) => text(await client.postToChannel(a.channel_id, a.body)));
124
+ 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.", {
125
+ channel_id: z.string().describe("Channel id, from list_channels."),
126
+ parent_id: z.string().describe("Parent message id, from read_channel."),
127
+ body: z.string().describe("Reply text, visible to all channel members."),
128
+ }, async (a) => text(await client.replyInChannel(a.channel_id, a.parent_id, a.body)));
129
+ // ── Routines (slash-command prompts) ────────────────────────────────────────
130
+ // Surface in the AI host as named slash commands (e.g. Claude Code:
131
+ // /productos:standup), so the human — or a scheduled headless run — can run a
132
+ // product routine on demand. Each orchestrates the tools above; they stay
133
+ // read-first and only write what the playbook sanctions.
134
+ 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` +
135
+ `1) pm_meta to resolve lists / statuses / members.\n` +
136
+ `2) list_tasks to read the active board.\n` +
137
+ `Report briefly: what's in progress, what's blocked (and why), what's gone stale with no movement, and the 1–3 ` +
138
+ `decisions that need a human today. Pull get_product_brain if revenue/account context sharpens the call. ` +
139
+ `End with the single most important thing to do next.`));
140
+ 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` +
141
+ `1) get_product_brain — ground in revenue, top accounts, recent verbatim signal, and open work.\n` +
142
+ `2) Review the board (list_tasks, pm_meta) and any fresh insights.\n` +
143
+ `3) Prioritize on EVIDENCE — affected accounts + MRR + reach — never an invented score.\n` +
144
+ `4) Make the writes you're confident in: create_task / update_task, linking insight→feature→task; ` +
145
+ `assign owners; place the clear ones in the current sprint. SURFACE the judgment calls for a human instead of guessing.\n` +
146
+ `Confirm only the writes you actually made.`));
147
+ 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` +
148
+ `1) get_product_brain for context.\n` +
149
+ `2) list_tasks + pm_meta — what's assigned to them, what's blocked, what's overdue or stale.\n` +
150
+ `3) Scan the support inbox (list_conversations) and upcoming calls (list_bookings) for anything urgent.\n` +
151
+ `Output a tight briefing: top priorities today, blockers needing a decision, and ONE recommended next action. ` +
152
+ `Read-only unless they ask you to act.`));
90
153
  async function main() {
91
154
  await server.connect(new StdioServerTransport());
92
155
  }
package/package.json CHANGED
@@ -1,17 +1,20 @@
1
1
  {
2
2
  "name": "@aioproductoscom/mcp",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
+ "mcpName": "com.aioproductos/mcp",
4
5
  "description": "ProductOS MCP — manage tickets, read your product brain + analytics, and run the support inbox from Claude Code, Cursor, Codex, and any MCP host.",
5
6
  "type": "module",
6
7
  "license": "MIT",
7
- "author": "AIITDEVELOPMENT DOO Novi Sad",
8
+ "author": "AIOProductOS Inc.",
8
9
  "homepage": "https://platform.aioproductos.com",
9
10
  "repository": {
10
11
  "type": "git",
11
12
  "url": "git+https://github.com/businesstalksnetwork/productos.git",
12
13
  "directory": "packages/mcps/productos-pm"
13
14
  },
14
- "bugs": { "url": "https://github.com/businesstalksnetwork/productos/issues" },
15
+ "bugs": {
16
+ "url": "https://github.com/businesstalksnetwork/productos/issues"
17
+ },
15
18
  "keywords": [
16
19
  "mcp",
17
20
  "model-context-protocol",
@@ -22,10 +25,19 @@
22
25
  "project-management",
23
26
  "product-management"
24
27
  ],
25
- "bin": { "productos": "dist/index.js" },
26
- "files": ["dist", "README.md"],
27
- "engines": { "node": ">=18" },
28
- "publishConfig": { "access": "public" },
28
+ "bin": {
29
+ "productos": "dist/index.js"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md"
34
+ ],
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
29
41
  "scripts": {
30
42
  "build": "tsc",
31
43
  "prepublishOnly": "npm run build",