@glie/mcp-polaris 0.13.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +20 -8
  2. package/dist/index.js +230 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -48,8 +48,10 @@ it. The same token authenticates this server and the `polaris` CLI as you.
48
48
  ## Tools
49
49
 
50
50
  **Roadmap.** `polaris.me`, `polaris.project.*`, `polaris.milestone.*` (incl.
51
- `update` to refine a spec, `attachment_*` to list/attach/remove the visual
52
- references anchored in its body), `polaris.task.*` (incl. `update`,
51
+ `update` to refine a spec or move its dates, `demote` to send one with no tasks back to the
52
+ idea list, `attachment_*` to list/attach/remove the visual
53
+ references anchored in its body), `polaris.idea.*` (incl. `promote` into a milestone and
54
+ `promote_task` into a standalone task with no milestone), `polaris.task.*` (incl. `update`,
53
55
  `transition`, `approve`, `schedule`, `attachment_*` — same as the milestone's,
54
56
  plus how a milestone reference gets forwarded here via
55
57
  `source_attachment_id`), `polaris.stack.*` (`list` / `get` / `release` for the
@@ -57,12 +59,22 @@ approval Release Candidate flow), `polaris.agent.*`, `polaris.team.*`,
57
59
  `polaris.claude_md.get`, `polaris.framework.list`.
58
60
 
59
61
  **Helpdesk.** `polaris.ticket.*` (the customer conversation: `reply`, `pass`,
60
- `frame_issue`, `link_issue`) and `polaris.issue.*` (the problem: `open`,
61
- `transition`, `add_task`). N tickets can dedup onto one issue, and one issue
62
- fans out into N tasks.
63
-
64
- **Context, read-only.** `polaris.activity.list` (what happened) and
65
- `polaris.chat.get` / `polaris.chat.messages` (comments agents left on a scope).
62
+ `frame_issue`, `link_issue`, `attachment_list`) and `polaris.issue.*` (the problem:
63
+ `open`, `transition`, `add_task`). N tickets can dedup onto one issue, and one issue
64
+ fans out into N tasks. A reply carries files through `attachment_ids`: upload each
65
+ one to `POST /api/tickets/:id/attachments` first (this server does not carry bytes),
66
+ then paste its `marker` into `body_md` to draw it inside the message.
67
+
68
+ **Context, read-only.** `polaris.activity.list` (what happened),
69
+ `polaris.chat.get` / `polaris.chat.messages` (comments agents left on a scope),
70
+ and `polaris.task.run_lines` (the thread of an agent's run on a task: what it was
71
+ asked, said, ran and got back; human tokens only).
72
+
73
+ **What a creative delivered.** `polaris.task.creatives` lists every version of a
74
+ creative task with its files and the words that go out beside them (caption,
75
+ title, copy). `polaris.task.creative_download` saves one version to this machine
76
+ as a zip of its slides, so a video can be opened, its frames pulled and its
77
+ sound measured.
66
78
 
67
79
  Run `tools/list` against the server for the full, current set.
68
80
 
package/dist/index.js CHANGED
@@ -2,6 +2,9 @@
2
2
  // @bun
3
3
 
4
4
  // ../../src/mcp/index.ts
5
+ import { mkdirSync, writeFileSync } from "fs";
6
+ import { tmpdir } from "os";
7
+ import { join, resolve } from "path";
5
8
  import { createInterface } from "readline";
6
9
 
7
10
  // ../../src/lib/media-types.ts
@@ -13,11 +16,22 @@ var MEDIA_TYPES = [
13
16
  { mime: "video/quicktime", kind: "video", ext: ".mov", aliases: [] },
14
17
  { mime: "video/webm", kind: "video", ext: ".webm", aliases: [] }
15
18
  ];
19
+ function mimesOfKind(kind) {
20
+ return MEDIA_TYPES.filter((t) => t.kind === kind).map((t) => t.mime);
21
+ }
16
22
  function allMimes() {
17
23
  return MEDIA_TYPES.map((t) => t.mime);
18
24
  }
25
+ var RECEIVED_IMAGE_MIMES = ["image/gif"];
26
+ var INLINE_MIMES = new Set([
27
+ ...mimesOfKind("image"),
28
+ ...mimesOfKind("video"),
29
+ ...RECEIVED_IMAGE_MIMES
30
+ ]);
19
31
 
20
32
  // ../../src/mcp/index.ts
33
+ var VERSION = "0.18.2";
34
+ var FRAMEWORK_KEY_DOC = "optional \u2014 a key from polaris.framework.list. Leave it out when none applies: a framework is for founding a large domain, and most milestones take none. An unknown key is refused (400) with the keys that exist.";
21
35
  var BASE = process.env.POLARIS_URL ?? "http://127.0.0.1:7843";
22
36
  var TOKEN = process.env.POLARIS_TOKEN ?? "";
23
37
  var PROJECT = process.env.POLARIS_PROJECT ?? "";
@@ -84,17 +98,24 @@ var INSTRUCTIONS = [
84
98
  "\u2022 N tickets \u2192 1 issue (duplicates dedup onto one problem) \xB7 1 ticket \u2192 N issues \xB7 1 issue \u2192 N tasks (iterative fix).",
85
99
  "\u2022 Flow: ticket.create \u2192 ticket.reply / ticket.pass (communicate) \u2192 ticket.frame_issue (or issue.open standalone; ticket.link_issue to dedup)",
86
100
  " \u2192 issue.add_task \xD7N (a planner decomposes) \u2192 task.transition the tasks through dev\u2192test\u2192approval\u2192release.",
87
- "\u2022 The ball: ticket.pass sets whose move it is (customer|team); a message never flips it. A customer reply auto-reopens a resolved/closed ticket.",
101
+ "\u2022 The ball: ticket.pass sets whose move it is (customer|team). A team message never moves it; a customer reply brings it back to the team and auto-reopens a resolved/closed ticket.",
88
102
  "",
89
103
  "WHICH TOOL WHEN:",
90
104
  `\u2022 "What's mine / what next?" \u2192 polaris.task.list (assignee \u2026="self", status=\u2026)`,
91
105
  '\u2022 "Read X in full" \u2192 polaris.task.get \xB7 milestone.get \xB7 project.get \xB7 stack.get',
92
106
  '\u2022 "Fix an unclear brief" \u2192 polaris.task.update / milestone.update (PATCH description_md)',
93
107
  '\u2022 "Catch up \u2014 what happened/said?"\u2192 polaris.activity.list (events) \xB7 polaris.chat.messages (comments left by agents)',
108
+ `\u2022 "What is the agent doing / why?" \u2192 polaris.task.run_lines (the run's thread: prompt, reasoning, commands, results; human-only)`,
109
+ '\u2022 "What did this creative deliver?" \u2192 polaris.task.creatives (versions, files, caption) \xB7 polaris.task.creative_download (the file, saved locally)',
94
110
  '\u2022 "Move my task forward" \u2192 polaris.task.transition(id, status)',
95
111
  `\u2022 "It's reviewed, ship it" \u2192 polaris.task.approve (human-only); ship several together via polaris.stack.* \u2192 stack.release`,
96
112
  `\u2022 "My task is stuck in a blocked stack" \u2192 fix the conflict on YOUR branch (rebase agent/task-N onto dev, push) \u2014 the rc PR flips to ready automatically. A stacked task can't advance individually; never merge the rc PR by hand.`,
97
113
  '\u2022 "Break a milestone into tasks" \u2192 polaris.milestone.qa_list / qa_answer (while in discovery)',
114
+ '\u2022 "Jot something down for later" \u2192 polaris.idea.create (a title is enough). NO dates: an idea is not a milestone.',
115
+ '\u2022 "What have we already thought of?" \u2192 polaris.idea.list \u2014 read it before proposing work; it also holds what was turned down',
116
+ '\u2022 "This idea is now real" \u2192 polaris.idea.promote (needs end_date + team) \u2014 the one door from the idea list to the timeline',
117
+ '\u2022 "This idea is one small piece of dev work" \u2192 polaris.idea.promote_task \u2014 a standalone task, no milestone, straight into the dev backlog (human-only)',
118
+ '\u2022 "This milestone was only a hunch" \u2192 polaris.milestone.demote \u2014 back to the idea list, only while it has no tasks (human-only)',
98
119
  `\u2022 "Triage support / what's waiting?" \u2192 polaris.ticket.list (status, project_key) \u2014 P1 first`,
99
120
  '\u2022 "Reply to a customer / hand off" \u2192 polaris.ticket.reply then polaris.ticket.pass(to)',
100
121
  '\u2022 "Turn a report into work" \u2192 polaris.ticket.frame_issue \u2192 polaris.issue.add_task \xD7N (framing is HUMAN-only; the helpdesker proposes it)',
@@ -162,16 +183,111 @@ var TOOLS = [
162
183
  },
163
184
  {
164
185
  name: "polaris.project.arm",
165
- description: "Arm a project for auto-approval (human-only): while armed, dev work that reaches `approval` CLEAN is approved on your behalf \u2014 no per-task click \u2014 and follows the normal path to release. Escalated tasks, mkt creatives, and support work still wait for a person; a merge that fails leaves the task in `approval`. Idempotent.",
186
+ description: "Arm a project for auto-approval (human-only): while armed, a task an agent writes in `draft` (discovery's batch, a campaign's pieces) is approved into `backlog` on your behalf, with no per-task click. Tasks a person creates by hand and support work still wait in `draft`. `approval \u2192 release` is never automatic. Not retroactive: drafts already there stay. Idempotent.",
166
187
  inputSchema: objSchema({ key: strSchema("project key (e.g. saasA)") }, ["key"]),
167
188
  call: async (args) => (await apiCall("POST", `/api/projects/${args.key}/arm`)).body
168
189
  },
169
190
  {
170
191
  name: "polaris.project.disarm",
171
- description: "Disarm a project (human-only). Immediate effect: the next task to reach `approval` waits for a person again. Work already parked in `approval` is untouched. Idempotent.",
192
+ description: "Disarm a project (human-only). Immediate effect: the next task an agent writes in `draft` waits for a person again. Tasks already moved to `backlog` stay there. Idempotent.",
172
193
  inputSchema: objSchema({ key: strSchema("project key (e.g. saasA)") }, ["key"]),
173
194
  call: async (args) => (await apiCall("POST", `/api/projects/${args.key}/disarm`)).body
174
195
  },
196
+ {
197
+ name: "polaris.idea.list",
198
+ description: "List a project's IDEAS \u2014 what might matter later, before it has a date. An idea is not a milestone: no dates, no tasks, never on the timeline. Read this before proposing new work: it is where the things already thought of (and the things already turned down) live. Optional status filter: open|promoted|dropped.",
199
+ inputSchema: objSchema({
200
+ project_key: strSchema("project key (e.g. saasA)"),
201
+ status: strSchema("open|promoted|dropped")
202
+ }, ["project_key"]),
203
+ call: async (args) => {
204
+ const qs = typeof args.status === "string" ? `?status=${args.status}` : "";
205
+ return (await apiCall("GET", `/api/projects/${args.project_key}/ideas${qs}`)).body;
206
+ }
207
+ },
208
+ {
209
+ name: "polaris.idea.create",
210
+ description: "Capture an idea in a project. The title is the only required field \u2014 capturing has to cost less than losing the idea. No dates: if it has a deadline it is a milestone, so use polaris.milestone.create instead. Needs a token.",
211
+ inputSchema: objSchema({
212
+ project_key: strSchema("project key (e.g. saasA)"),
213
+ title: strSchema("one line \u2014 what the idea is"),
214
+ note_md: strSchema("optional: what it is and why now; becomes the milestone's description on promotion"),
215
+ team: strSchema("optional: dev|mkt \u2014 leave empty if you don't know yet")
216
+ }, ["project_key", "title"]),
217
+ call: async (args) => {
218
+ const { project_key, ...body } = args;
219
+ return (await apiCall("POST", `/api/projects/${project_key}/ideas`, body)).body;
220
+ }
221
+ },
222
+ {
223
+ name: "polaris.idea.update",
224
+ description: "Refine an open idea \u2014 its title, note or team. A promoted idea is refused: it became a milestone, and the milestone is the truth from then on (use polaris.milestone.update). Needs a token.",
225
+ inputSchema: objSchema({
226
+ project_key: strSchema(),
227
+ id: intSchema("idea id"),
228
+ title: strSchema(),
229
+ note_md: strSchema(),
230
+ team: strSchema("dev|mkt")
231
+ }, ["project_key", "id"]),
232
+ call: async (args) => {
233
+ const { project_key, id, ...body } = args;
234
+ return (await apiCall("PATCH", `/api/projects/${project_key}/ideas/${id}`, body)).body;
235
+ }
236
+ },
237
+ {
238
+ name: "polaris.idea.promote",
239
+ description: "Promote an idea into a milestone \u2014 the one door between the idea list and the timeline. Demands what a milestone demands: end_date, and a team (unless the idea already carries one). The milestone is created and the idea stamped in ONE transaction; promoting twice returns the first milestone and writes nothing. Needs a token.",
240
+ inputSchema: objSchema({
241
+ project_key: strSchema(),
242
+ id: intSchema("idea id"),
243
+ end_date: strSchema("YYYY-MM-DD \u2014 the deadline the milestone commits to"),
244
+ team: strSchema("dev|mkt \u2014 required unless the idea already has one"),
245
+ start_date: strSchema("YYYY-MM-DD, optional"),
246
+ name: strSchema("optional: overrides the idea's title"),
247
+ description_md: strSchema("optional: overrides the idea's note"),
248
+ framework_key: strSchema(FRAMEWORK_KEY_DOC)
249
+ }, ["project_key", "id", "end_date"]),
250
+ call: async (args) => {
251
+ const { project_key, id, ...body } = args;
252
+ return (await apiCall("POST", `/api/projects/${project_key}/ideas/${id}/promote`, body)).body;
253
+ }
254
+ },
255
+ {
256
+ name: "polaris.idea.promote_task",
257
+ description: "Promote a dev idea into a STANDALONE task \u2014 one piece of work with no milestone and no deadline, born in the idea's project and straight into the dev backlog, where the project's developer agents pick it up. For work that is a goal with several tasks, use polaris.idea.promote (a milestone). A mkt idea is refused: a creative needs a campaign. Promoting twice returns the first task and writes nothing. Human-only.",
258
+ inputSchema: objSchema({
259
+ project_key: strSchema(),
260
+ id: intSchema("idea id"),
261
+ name: strSchema("optional: overrides the idea's title"),
262
+ description_md: strSchema("optional: overrides the idea's note (the developer's brief)")
263
+ }, ["project_key", "id"]),
264
+ call: async (args) => {
265
+ const { project_key, id, ...body } = args;
266
+ return (await apiCall("POST", `/api/projects/${project_key}/ideas/${id}/promote-task`, body)).body;
267
+ }
268
+ },
269
+ {
270
+ name: "polaris.idea.drop",
271
+ description: "Drop an idea, with the reason it was dropped. Reversible (polaris.idea.reopen) \u2014 dropping is not a one-way door, because almost every good idea looks bad at first. Idempotent: dropping twice keeps the first reason. Needs a token.",
272
+ inputSchema: objSchema({
273
+ project_key: strSchema(),
274
+ id: intSchema("idea id"),
275
+ reason: strSchema("why it was dropped")
276
+ }, ["project_key", "id"]),
277
+ call: async (args) => {
278
+ const { project_key, id, ...body } = args;
279
+ return (await apiCall("POST", `/api/projects/${project_key}/ideas/${id}/drop`, body)).body;
280
+ }
281
+ },
282
+ {
283
+ name: "polaris.idea.reopen",
284
+ description: "Put a dropped idea back on the open list, clearing its reason. Required before promoting a dropped idea \u2014 resurrecting is a decision, not a side effect. Needs a token.",
285
+ inputSchema: objSchema({ project_key: strSchema(), id: intSchema("idea id") }, [
286
+ "project_key",
287
+ "id"
288
+ ]),
289
+ call: async (args) => (await apiCall("POST", `/api/projects/${args.project_key}/ideas/${args.id}/reopen`, {})).body
290
+ },
175
291
  {
176
292
  name: "polaris.project.pause_publishing",
177
293
  description: "Pause publishing through Polaris for a product (human-only). While paused, no creative of this product goes out at its scheduled hour. It does NOT hold the clock: a piece whose hour passes meanwhile comes back to the person in `release`, like any other late piece \u2014 that is what pausing is for. Idempotent: a second call keeps the original pause, and the original reason. `reason` is optional and is what whoever finds it paused reads.",
@@ -191,9 +307,21 @@ var TOOLS = [
191
307
  },
192
308
  {
193
309
  name: "polaris.milestone.list",
194
- description: "List milestones in a project.",
195
- inputSchema: objSchema({ project_key: strSchema() }, ["project_key"]),
196
- call: async (args) => (await apiCall("GET", `/api/projects/${args.project_key}/milestones?limit=200`)).body
310
+ description: "List milestones in a project. Each one comes with its whole description, so a project with a long history answers with a lot: narrow it with `status` (e.g. loop for the campaigns running now) and `team` (dev|mkt).",
311
+ inputSchema: objSchema({
312
+ project_key: strSchema(),
313
+ status: strSchema("draft|discovery|loop|release|done|canceled"),
314
+ team: strSchema("dev|mkt")
315
+ }, ["project_key"]),
316
+ call: async (args) => {
317
+ const qs = new URLSearchParams({ limit: "200" });
318
+ for (const k of ["status", "team"]) {
319
+ const v = args[k];
320
+ if (v !== undefined && v !== null)
321
+ qs.set(k, String(v));
322
+ }
323
+ return (await apiCall("GET", `/api/projects/${args.project_key}/milestones?${qs}`)).body;
324
+ }
197
325
  },
198
326
  {
199
327
  name: "polaris.milestone.get",
@@ -203,11 +331,13 @@ var TOOLS = [
203
331
  },
204
332
  {
205
333
  name: "polaris.milestone.update",
206
- description: "Correct or refine a milestone's spec \u2014 PATCH its description_md (and optionally name), same idea as polaris.task.update. On a mkt campaign the market can also be corrected, while it is still in draft or discovery: after that the creatives are already bound to accounts. The campaign's rhythm, budget and channels live in the description \u2014 write them there and the briefer reads them. Needs a token.",
334
+ description: "Correct or refine a milestone \u2014 PATCH its description_md, name, or dates (start_date / end_date move it on the timeline; send only the one that changes, start can never fall after end), same idea as polaris.task.update. On a mkt campaign the market can also be corrected, while it is still in draft or discovery: after that the creatives are already bound to accounts. The campaign's rhythm, budget and channels live in the description \u2014 write them there and the briefer reads them. Needs a token.",
207
335
  inputSchema: objSchema({
208
336
  id: intSchema(),
209
337
  name: strSchema(),
210
338
  description_md: strSchema("full markdown brief; replaces the current description"),
339
+ start_date: strSchema("ISO date, e.g. 2026-06-01"),
340
+ end_date: strSchema("ISO date, the deadline, e.g. 2026-06-30"),
211
341
  market: {
212
342
  type: "array",
213
343
  items: { type: "string", enum: ["BR", "US", "EU"] },
@@ -220,6 +350,10 @@ var TOOLS = [
220
350
  body.name = args.name;
221
351
  if (typeof args.description_md === "string")
222
352
  body.description_md = args.description_md;
353
+ if (typeof args.start_date === "string")
354
+ body.start_date = args.start_date;
355
+ if (typeof args.end_date === "string")
356
+ body.end_date = args.end_date;
223
357
  if ("market" in args)
224
358
  body.market = args.market;
225
359
  return (await apiCall("PATCH", `/api/milestones/${args.id}`, body)).body;
@@ -231,6 +365,12 @@ var TOOLS = [
231
365
  inputSchema: objSchema({ id: intSchema(), status: strSchema() }, ["id", "status"]),
232
366
  call: async (args) => (await apiCall("POST", `/api/milestones/${args.id}/state`, { status: args.status })).body
233
367
  },
368
+ {
369
+ name: "polaris.milestone.demote",
370
+ description: "Send a milestone back to the idea list. Only a milestone with NO tasks (not even canceled ones), still in draft, discovery or loop, and not while a planner is breaking it into tasks. The idea it was promoted from reopens (or a new idea is captured from it) carrying the milestone's name and context; the milestone is canceled and cannot be reopened \u2014 promote the idea again instead. Demoting twice returns the first idea. Human-only.",
371
+ inputSchema: objSchema({ id: intSchema("milestone id") }, ["id"]),
372
+ call: async (args) => (await apiCall("POST", `/api/milestones/${args.id}/demote`, {})).body
373
+ },
234
374
  {
235
375
  name: "polaris.milestone.qa_list",
236
376
  description: "List framework Q&A rows for a milestone.",
@@ -305,10 +445,10 @@ var TOOLS = [
305
445
  },
306
446
  {
307
447
  name: "polaris.task.list",
308
- description: `Find tasks \u2014 the agent's entry point. Pass assignee_agent_id="self" (or assignee_human_id="self" for a human) to get what is YOURS; add status=dev (etc.) to see only active work. Other filters: milestone_id, product_key, team.`,
448
+ description: `Find tasks \u2014 the agent's entry point. Pass assignee_agent_id="self" (or assignee_human_id="self" for a human) to get what is YOURS; add status=dev (etc.) to see only active work. Other filters: milestone_id (or "none" for the standalone tasks, which have no milestone), product_key, team.`,
309
449
  inputSchema: objSchema({
310
450
  status: strSchema("comma-separated subset of: draft,backlog,dev,test,approval,release,done,canceled"),
311
- milestone_id: intSchema(),
451
+ milestone_id: strSchema('a milestone id, or "none" for the standalone tasks (no milestone)'),
312
452
  product_key: strSchema("project key, e.g. saasA"),
313
453
  team: strSchema("dev|mkt"),
314
454
  assignee_agent_id: strSchema('agent id, or "self" for the calling agent'),
@@ -342,9 +482,10 @@ var TOOLS = [
342
482
  },
343
483
  {
344
484
  name: "polaris.task.create",
345
- description: "Create a task within a milestone. A marketing CREATIVE (a task in an mkt campaign) carries WHERE IT GOES: `presence_id`, one of the destinations from polaris.destination.list. The channel and the market are read from that destination, and sending them here is refused. It also takes format + scheduled_date (the publish day / calendar slot) + platform (if this creative is individually paid). A CAROUSEL (format=carrossel) also requires `slides` (2..10) \u2014 without it the call is refused, because a carousel with no declared size ships one image.",
485
+ description: "Create a task within a milestone \u2014 or, for one small piece of DEV work with no milestone, a STANDALONE task in a project: send project_key instead of milestone_id (human-only, team dev, no creative fields). A marketing CREATIVE (a task in an mkt campaign) carries WHERE IT GOES: `presence_id`, one of the destinations from polaris.destination.list. The channel and the market are read from that destination, and sending them here is refused. It also takes format + scheduled_date (the publish day / calendar slot) + platform (if this creative is individually paid). A CAROUSEL (format=carrossel) also requires `slides` (2..10) \u2014 without it the call is refused, because a carousel with no declared size ships one image.",
346
486
  inputSchema: objSchema({
347
- milestone_id: intSchema(),
487
+ milestone_id: intSchema("the milestone it belongs to \u2014 or omit it and send project_key"),
488
+ project_key: strSchema("instead of milestone_id: a standalone dev task in this project, with no milestone (human-only)"),
348
489
  name: strSchema(),
349
490
  description_md: strSchema(),
350
491
  status: strSchema("draft \u2014 the only status a task can be created in; it reaches the rest by being moved, one legal step at a time (polaris.task.transition)"),
@@ -359,7 +500,7 @@ var TOOLS = [
359
500
  scheduled_date: strSchema("mkt: publish day (ISO date) \u2014 the calendar slot"),
360
501
  platform: strSchema("mkt: ad platform if this creative is individually paid"),
361
502
  asset_url: strSchema("mkt: URL of the produced creative asset (image/video) for the reviewer")
362
- }, ["milestone_id", "name"]),
503
+ }, ["name"]),
363
504
  call: async (args) => (await apiCall("POST", "/api/tasks", args)).body
364
505
  },
365
506
  {
@@ -535,13 +676,29 @@ var TOOLS = [
535
676
  },
536
677
  {
537
678
  name: "polaris.ticket.reply",
538
- description: "Add a message to a ticket's conversation. Default side=team (you replying); an adapter relaying a customer message passes side=customer. Sending a message does NOT pass the ball.",
539
- inputSchema: objSchema({ id: intSchema(), body_md: strSchema(), side: strSchema("customer|team") }, ["id", "body_md"]),
679
+ description: "Add a message to a ticket's conversation. Default side=team (you replying); an adapter relaying a customer message passes side=customer. Sending a message does NOT pass the ball. To send FILES with the reply (a screenshot, a screencast of the thing you are describing): upload each one first \u2014 `curl -sS -H \"Authorization: Bearer $POLARIS_TOKEN\" -F file=@clip.mp4 $POLARIS_URL/api/tickets/<id>/attachments` \u2014 then pass the ids it returns as attachment_ids. Paste each one's `marker` into body_md where it belongs and the image or video is drawn THERE, between your paragraphs; an attachment whose marker you never placed falls to a strip at the foot of the message, which for a screencast is the wrong place. Up to 15MB per file.",
680
+ inputSchema: objSchema({
681
+ id: intSchema(),
682
+ body_md: strSchema(),
683
+ side: strSchema("customer|team"),
684
+ attachment_ids: {
685
+ type: "array",
686
+ items: { type: "integer" },
687
+ description: "Ids from the attachment upload above (or from ticket.attachment_list, where sent=false). They ride on THIS message and become visible with it."
688
+ }
689
+ }, ["id", "body_md"]),
540
690
  call: async (args) => (await apiCall("POST", `/api/tickets/${args.id}/messages`, {
541
691
  body_md: args.body_md,
542
- side: args.side
692
+ side: args.side,
693
+ attachment_ids: args.attachment_ids
543
694
  })).body
544
695
  },
696
+ {
697
+ name: "polaris.ticket.attachment_list",
698
+ description: 'Every file on this ticket \u2014 what the customer sent and what we have attached. Each row carries a `marker` (`![name](ref:N)` for an image, `[name](ref:N)` for a video) to paste into a reply\'s body_md so the file is drawn inside the message instead of in a list under it, and `sent` (false while it has been uploaded and no message names it yet \u2014 nobody sees it until a reply does). A file (PDF, log) has no marker: there is nothing to draw, and it rides as a download chip. Upload with `curl -sS -H "Authorization: Bearer $POLARIS_TOKEN" -F file=@shot.png $POLARIS_URL/api/tickets/<id>/attachments` \u2014 this call does not carry bytes.',
699
+ inputSchema: objSchema({ id: intSchema() }, ["id"]),
700
+ call: async (args) => (await apiCall("GET", `/api/tickets/${args.id}/attachments`)).body
701
+ },
545
702
  {
546
703
  name: "polaris.ticket.pass",
547
704
  description: "Pass the ball \u2014 whose move it is. to=customer after you reply; to=team to pull it back. Manual + idempotent.",
@@ -752,7 +909,7 @@ var TOOLS = [
752
909
  },
753
910
  {
754
911
  name: "polaris.chat.messages",
755
- description: "Read the comments/context agents and humans left on a scope (e.g. task 42) \u2014 free-text notes, decisions, @mentions. Pass the scope; the chat id is resolved for you. Read-only (posting isn't exposed yet).",
912
+ description: "Read the comments/context agents and humans left on a scope (e.g. task 42) \u2014 free-text notes, decisions, @mentions, and the handover notes that travel with a task's moves (what the maker delivered, why a reviewer sent it back). Pass the scope; the chat id is resolved for you. To write one, polaris.chat.post.",
756
913
  inputSchema: objSchema({
757
914
  scope_type: strSchema("task|milestone|project|agent_dm|human_dm|general"),
758
915
  scope_id: intSchema(),
@@ -767,6 +924,58 @@ var TOOLS = [
767
924
  return (await apiCall("GET", `/api/chats/${chatId}/messages${qs}`)).body;
768
925
  }
769
926
  },
927
+ {
928
+ name: "polaris.task.run_lines",
929
+ description: "Watch an agent work: the THREAD of one run on a task \u2014 what it was asked (`prompt`), what it said (`said`), what it ran (`ran`, tool + full input), what came back (`back`), and the rest (`other`). This is the backstage of a run; for the comments left on the task use polaris.chat.messages. HUMAN ONLY: an agent token gets 403 (one agent reading another's reasoning changes what it can judge). A task has one run per agent session (the brief, whoever made the piece, the reviewer); the response's `runs` lists them, newest first, and without `session` you get the newest. Bodies are whole, never truncated, and the first line is the entire prompt \u2014 page with `limit`, then pass the response's `next_after` as `after` to read on or to follow a live run.",
930
+ inputSchema: objSchema({
931
+ id: intSchema("task id"),
932
+ session: strSchema("a session_id from `runs` \u2014 which run to read; omit for the newest"),
933
+ after: intSchema("return only lines with seq greater than this (the previous `next_after`)"),
934
+ limit: intSchema("how many lines (default 500, max 2000)")
935
+ }, ["id"]),
936
+ call: async (args) => {
937
+ const qs = new URLSearchParams;
938
+ for (const k of ["session", "after", "limit"]) {
939
+ const v = args[k];
940
+ if (v !== undefined && v !== null)
941
+ qs.set(k, String(v));
942
+ }
943
+ const r = await apiCall("GET", `/api/tasks/${args.id}/run-lines${qs.toString() ? `?${qs}` : ""}`);
944
+ return r.body;
945
+ }
946
+ },
947
+ {
948
+ name: "polaris.task.creatives",
949
+ description: "What a creative task has actually delivered: every version, newest first, each slot with its file (`mime`, `url`, `status`) and the words that go out beside it (`caption`, `title`, `copy`, `description`). Read this to judge a piece; the maker's handover note is in polaris.chat.messages on the task. The files sit behind the same login as the API, so to look at one, polaris.task.creative_download.",
950
+ inputSchema: objSchema({ id: intSchema("task id") }, ["id"]),
951
+ call: async (args) => (await apiCall("GET", `/api/tasks/${args.id}/creatives`)).body
952
+ },
953
+ {
954
+ name: "polaris.task.creative_download",
955
+ description: "Save one version of a creative to THIS machine, as a zip of its slides in slot order (slide-01.mp4, slide-02.png, ...), so its frames can be pulled and its sound measured. Newest version unless `version` is given; only a version whose slides are all ready exists to download (404 otherwise). Writes into `dir` (default: the system temp folder) and returns the file's absolute path and size; unzip it to look inside.",
956
+ inputSchema: objSchema({
957
+ id: intSchema("task id"),
958
+ version: intSchema("which version (polaris.task.creatives lists them); omit for the newest"),
959
+ dir: strSchema("folder to save into; created if missing. Default: the system temp folder")
960
+ }, ["id"]),
961
+ call: async (args) => {
962
+ const qs = typeof args.version === "number" ? `?version=${args.version}` : "";
963
+ const headers = {};
964
+ if (TOKEN)
965
+ headers.Authorization = `Bearer ${TOKEN}`;
966
+ const res = await fetch(`${BASE}/api/tasks/${args.id}/creative.zip${qs}`, { headers });
967
+ if (!res.ok) {
968
+ return { status: res.status, body: await res.text() };
969
+ }
970
+ const bytes = new Uint8Array(await res.arrayBuffer());
971
+ const dir = resolve(typeof args.dir === "string" && args.dir ? args.dir : tmpdir());
972
+ mkdirSync(dir, { recursive: true });
973
+ const name = `task-${args.id}-${typeof args.version === "number" ? `v${args.version}` : "latest"}.zip`;
974
+ const path = join(dir, name);
975
+ writeFileSync(path, bytes);
976
+ return { data: { path, bytes: bytes.length } };
977
+ }
978
+ },
770
979
  {
771
980
  name: "polaris.project.update",
772
981
  description: "Update a project (human-only): repo_url, name, status (active|paused|archived), owner_human_id. Brand/context live in the CLAUDE.md \u2014 use polaris.project.claude_md_set.",
@@ -987,7 +1196,7 @@ var TOOLS = [
987
1196
  team: strSchema("dev|mkt"),
988
1197
  end_date: strSchema("ISO date, e.g. 2026-06-30"),
989
1198
  start_date: strSchema("ISO date (optional)"),
990
- framework_key: strSchema(),
1199
+ framework_key: strSchema(FRAMEWORK_KEY_DOC),
991
1200
  description_md: strSchema(),
992
1201
  market: {
993
1202
  type: "array",
@@ -1094,7 +1303,7 @@ var TOOLS = [
1094
1303
  items: { type: "string" },
1095
1304
  description: "developer|tester|planner|helpdesker"
1096
1305
  },
1097
- framework_key: strSchema()
1306
+ framework_key: strSchema("optional \u2014 the team's default framework: a key from polaris.framework.list")
1098
1307
  }, ["key", "name"]),
1099
1308
  call: async (args) => (await apiCall("POST", "/api/teams", args)).body
1100
1309
  },
@@ -1106,7 +1315,7 @@ var TOOLS = [
1106
1315
  name: strSchema(),
1107
1316
  requires: { type: "array", items: { type: "string" } },
1108
1317
  functions: { type: "array", items: { type: "string" } },
1109
- framework_key: strSchema()
1318
+ framework_key: strSchema("the team's default framework: a key from polaris.framework.list")
1110
1319
  }, ["key"]),
1111
1320
  call: async (args) => {
1112
1321
  const { key, ...patch } = args;
@@ -1132,7 +1341,7 @@ async function handle(req) {
1132
1341
  return ok(id, {
1133
1342
  protocolVersion: "2024-11-05",
1134
1343
  capabilities: { tools: {} },
1135
- serverInfo: { name: "mcp-polaris", version: "0.3.0" },
1344
+ serverInfo: { name: "mcp-polaris", version: VERSION },
1136
1345
  instructions: INSTRUCTIONS
1137
1346
  });
1138
1347
  case "initialized":
@@ -1192,5 +1401,6 @@ if (true) {
1192
1401
  rl.on("close", () => log("stdin closed, exiting"));
1193
1402
  }
1194
1403
  export {
1404
+ VERSION,
1195
1405
  TOOLS
1196
1406
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glie/mcp-polaris",
3
- "version": "0.13.0",
3
+ "version": "0.18.2",
4
4
  "description": "Model Context Protocol (stdio) server exposing Polaris, glie's ops platform, as native tools in Claude Code and any MCP client.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "glie <carlos@glie.ai>",