pipebrain-mcp 0.31.0 → 0.33.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/main.js +218 -65
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -356,9 +356,52 @@ var require_main = __commonJS({
|
|
|
356
356
|
}
|
|
357
357
|
});
|
|
358
358
|
|
|
359
|
+
// ../../libs/agent-shared/dist/agent-config.js
|
|
360
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
361
|
+
import { homedir } from "node:os";
|
|
362
|
+
import { join } from "node:path";
|
|
363
|
+
|
|
364
|
+
// ../../libs/agent-shared/dist/agent-config-manifest.js
|
|
365
|
+
var AGENT_CONFIG_DIGEST = "898da8356dac55bd14531dcb5440aa3bd7bbcaa72e873d0fee4a102e3d718ee1";
|
|
366
|
+
|
|
367
|
+
// ../../libs/agent-shared/dist/agent-config.js
|
|
368
|
+
function agentConfigDir(env = process.env) {
|
|
369
|
+
const configured = env.CLAUDE_CONFIG_DIR;
|
|
370
|
+
return configured !== void 0 && configured !== "" ? configured : join(homedir(), ".claude");
|
|
371
|
+
}
|
|
372
|
+
var STAMP_REL = "pipebrain/installed.json";
|
|
373
|
+
function readInstalledStamp(dir) {
|
|
374
|
+
const file2 = join(dir, STAMP_REL);
|
|
375
|
+
if (!existsSync(file2))
|
|
376
|
+
return null;
|
|
377
|
+
try {
|
|
378
|
+
const parsed = JSON.parse(readFileSync(file2, "utf8"));
|
|
379
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
380
|
+
return null;
|
|
381
|
+
const stamp = parsed;
|
|
382
|
+
return typeof stamp.configDigest === "string" && stamp.configDigest !== "" ? {
|
|
383
|
+
configDigest: stamp.configDigest,
|
|
384
|
+
installedBy: typeof stamp.installedBy === "string" ? stamp.installedBy : "unknown",
|
|
385
|
+
installedAt: typeof stamp.installedAt === "string" ? stamp.installedAt : "unknown"
|
|
386
|
+
} : null;
|
|
387
|
+
} catch {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function agentConfigStatus(dir, shipped = AGENT_CONFIG_DIGEST) {
|
|
392
|
+
const stamp = readInstalledStamp(dir);
|
|
393
|
+
if (stamp === null)
|
|
394
|
+
return "absent";
|
|
395
|
+
return stamp.configDigest === shipped ? "current" : "behind";
|
|
396
|
+
}
|
|
397
|
+
var UPDATE_COMMAND = "ppb setup";
|
|
398
|
+
function agentConfigNotice(status) {
|
|
399
|
+
return status === "behind" ? `This machine's shared agent configuration is not the one this build ships \u2014 run \`${UPDATE_COMMAND}\` to see what would change and update it.` : null;
|
|
400
|
+
}
|
|
401
|
+
|
|
359
402
|
// ../../libs/agent-shared/dist/asset-transfer.js
|
|
360
403
|
import { createHash } from "node:crypto";
|
|
361
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
404
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
362
405
|
import { basename, extname } from "node:path";
|
|
363
406
|
var MIME_BY_EXT = {
|
|
364
407
|
".png": "image/png",
|
|
@@ -381,9 +424,9 @@ function guessContentType(filename) {
|
|
|
381
424
|
return MIME_BY_EXT[extname(filename).toLowerCase()] ?? "application/octet-stream";
|
|
382
425
|
}
|
|
383
426
|
async function uploadAssetFromPath(client2, apiBaseUrl, input) {
|
|
384
|
-
if (!
|
|
427
|
+
if (!existsSync2(input.path))
|
|
385
428
|
throw new Error(`file not found: ${input.path}`);
|
|
386
|
-
const bytes =
|
|
429
|
+
const bytes = readFileSync2(input.path);
|
|
387
430
|
const filename = basename(input.path);
|
|
388
431
|
const contentType = input.contentType ?? guessContentType(filename);
|
|
389
432
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
@@ -15589,6 +15632,22 @@ var orgInstructionSchema = external_exports.object({
|
|
|
15589
15632
|
body: external_exports.string().min(1),
|
|
15590
15633
|
createdBy: uuidSchema.nullable(),
|
|
15591
15634
|
viaTokenId: uuidSchema.nullable(),
|
|
15635
|
+
/**
|
|
15636
|
+
* sha256 of the shipped default this row was SEEDED from, or null when nothing seeded
|
|
15637
|
+
* it — a row written by a person, a row re-created after its scope was cleared, or a row
|
|
15638
|
+
* that predates seeding.
|
|
15639
|
+
*
|
|
15640
|
+
* It is what lets a settings page say the shipped default has MOVED since this
|
|
15641
|
+
* organization got it, without the product ever applying that move: comparing this digest
|
|
15642
|
+
* against the running release's default for the same scope is the whole signal. It is not
|
|
15643
|
+
* updated by an edit, because what it records is what was seeded — which stays true after
|
|
15644
|
+
* the organization rewrites the body, and is exactly then that the comparison is worth
|
|
15645
|
+
* making.
|
|
15646
|
+
*
|
|
15647
|
+
* `.default(null)` rather than a bare nullable, for the tolerance this schema keeps
|
|
15648
|
+
* throughout: a payload from a server that predates the column still parses.
|
|
15649
|
+
*/
|
|
15650
|
+
seededDefaultSha: external_exports.string().nullable().default(null),
|
|
15592
15651
|
createdAt: external_exports.iso.datetime({ offset: true }),
|
|
15593
15652
|
updatedAt: external_exports.iso.datetime({ offset: true }),
|
|
15594
15653
|
deletedAt: external_exports.iso.datetime({ offset: true }).nullable()
|
|
@@ -16105,6 +16164,12 @@ var listEnvelopeSchema = (item) => external_exports.object({ items: external_exp
|
|
|
16105
16164
|
var mintedTokenSchema = apiTokenSchema.extend({ token: external_exports.string().min(1) }).loose();
|
|
16106
16165
|
var tokenListSchema = listEnvelopeSchema(apiTokenSchema);
|
|
16107
16166
|
var orgInstructionListSchema = listEnvelopeSchema(orgInstructionSchema);
|
|
16167
|
+
var instructionDefaultSchema = external_exports.object({
|
|
16168
|
+
scope: external_exports.string().min(1),
|
|
16169
|
+
body: external_exports.string().min(1),
|
|
16170
|
+
sha256: external_exports.string().min(1)
|
|
16171
|
+
}).loose();
|
|
16172
|
+
var instructionDefaultListSchema = listEnvelopeSchema(instructionDefaultSchema);
|
|
16108
16173
|
var ApiClient = class _ApiClient {
|
|
16109
16174
|
baseUrl;
|
|
16110
16175
|
auth;
|
|
@@ -16715,6 +16780,18 @@ var ApiClient = class _ApiClient {
|
|
|
16715
16780
|
const body = await this.request("GET", "/api/v1/org-instructions", {}, orgInstructionListSchema);
|
|
16716
16781
|
return body.items;
|
|
16717
16782
|
}
|
|
16783
|
+
/**
|
|
16784
|
+
* The starting text THIS RELEASE ships, per scope, with the digest a seeded row records.
|
|
16785
|
+
*
|
|
16786
|
+
* Shipped content rather than the org's, so it carries no tenant data and any
|
|
16787
|
+
* authenticated caller may read it. Comparing a row's `seededDefaultSha` against the
|
|
16788
|
+
* `sha256` here is how a settings page says the shipped default has MOVED since this
|
|
16789
|
+
* organization was seeded — a difference the product reports and never applies.
|
|
16790
|
+
*/
|
|
16791
|
+
async listInstructionDefaults() {
|
|
16792
|
+
const body = await this.request("GET", "/api/v1/org-instructions/defaults", {}, instructionDefaultListSchema);
|
|
16793
|
+
return body.items;
|
|
16794
|
+
}
|
|
16718
16795
|
/** Upserts one scope's instruction text (ADMIN+; body ≤ 4000 chars). */
|
|
16719
16796
|
setOrgInstruction(scope, body) {
|
|
16720
16797
|
return this.request("PUT", `/api/v1/org-instructions/${enc(scope)}`, { body: { body } }, orgInstructionSchema);
|
|
@@ -16861,6 +16938,26 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
16861
16938
|
|
|
16862
16939
|
// ../../libs/mcp-server/dist/instructions.js
|
|
16863
16940
|
var STARTUP_FETCH_TIMEOUT_MS = 5e3;
|
|
16941
|
+
var WORK_CONTEXT_RESULT_CHARS = 2e5;
|
|
16942
|
+
var GUIDANCE_POINTER = "The binding guidance for writing here \u2014 the product doctrine and this organization\u2019s own writing rules \u2014 is returned by `get_work_context`. Read it before you write.";
|
|
16943
|
+
function bindingBanner(apiBaseUrl) {
|
|
16944
|
+
return `Pipebrain MCP server, bound to the ${apiBindingLabel(apiBaseUrl)} API at ${apiBaseUrl}. Every tool reads and writes THAT environment \u2014 confirm it is the one you intend before mutating.`;
|
|
16945
|
+
}
|
|
16946
|
+
var INDEX_RULES = "Until you have it: an open decision is filed as a question with `ask_questions` and never written into a body; search for an existing record before creating one, because a duplicate splits one thing\u2019s history across two identifiers and nothing merges them afterward; and a secret value never travels in a tool call, because a value in a call is disclosed before the server can encrypt it.";
|
|
16947
|
+
function composeIndex(base, machineNotice2 = null) {
|
|
16948
|
+
return [
|
|
16949
|
+
base,
|
|
16950
|
+
"Call `get_work_context` before you write anything \u2014 it returns the binding guidance, the product doctrine and this organization\u2019s own writing rules, together with the work. Nothing else delivers that guidance in full; every other read only names this call.",
|
|
16951
|
+
INDEX_RULES,
|
|
16952
|
+
// LAST, and usually absent. It is about the MACHINE rather than about the work, so it
|
|
16953
|
+
// must never displace a rule: a squeezed share keeps the prefix, and losing a sentence
|
|
16954
|
+
// about an installer costs less than losing the sentence that recovers everything else.
|
|
16955
|
+
// Present only when this machine's shared configuration is not the one this build ships
|
|
16956
|
+
// (PIPE-354 AC-5) — a machine that never installed it is passed over in silence, since
|
|
16957
|
+
// it has no configuration to be behind with.
|
|
16958
|
+
...machineNotice2 === null ? [] : [machineNotice2]
|
|
16959
|
+
].join("\n\n");
|
|
16960
|
+
}
|
|
16864
16961
|
async function fetchOrgInstructionsSafe(client2) {
|
|
16865
16962
|
let timer;
|
|
16866
16963
|
try {
|
|
@@ -16904,9 +17001,21 @@ function successorNotice() {
|
|
|
16904
17001
|
const armed = process.env.PIPELINE_MCP_SUCCESSOR_NOTICE;
|
|
16905
17002
|
return armed === void 0 || armed === "" ? "" : SUCCESSOR_NOTICE;
|
|
16906
17003
|
}
|
|
17004
|
+
function machineNotice() {
|
|
17005
|
+
try {
|
|
17006
|
+
return agentConfigNotice(agentConfigStatus(agentConfigDir()));
|
|
17007
|
+
} catch {
|
|
17008
|
+
return null;
|
|
17009
|
+
}
|
|
17010
|
+
}
|
|
16907
17011
|
var DEFAULT_VERSION = "0.0.0";
|
|
16908
|
-
function ok(data) {
|
|
16909
|
-
|
|
17012
|
+
function ok(data, appended) {
|
|
17013
|
+
const content = [
|
|
17014
|
+
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
17015
|
+
];
|
|
17016
|
+
if (appended !== void 0)
|
|
17017
|
+
content.push({ type: "text", text: appended });
|
|
17018
|
+
return { content };
|
|
16910
17019
|
}
|
|
16911
17020
|
function toolError(err) {
|
|
16912
17021
|
if (err instanceof ApiError) {
|
|
@@ -16921,9 +17030,9 @@ code: ${err.code}${detailText}` }]
|
|
|
16921
17030
|
const message = err instanceof Error ? err.message : String(err);
|
|
16922
17031
|
return { isError: true, content: [{ type: "text", text: message }] };
|
|
16923
17032
|
}
|
|
16924
|
-
async function run(body) {
|
|
17033
|
+
async function run(body, appended) {
|
|
16925
17034
|
try {
|
|
16926
|
-
return ok(await body());
|
|
17035
|
+
return ok(await body(), appended);
|
|
16927
17036
|
} catch (err) {
|
|
16928
17037
|
return toolError(err);
|
|
16929
17038
|
}
|
|
@@ -16956,14 +17065,22 @@ async function resolveDeliverableTargetId(client2, projectKey, ref) {
|
|
|
16956
17065
|
}
|
|
16957
17066
|
function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */ new Map(), opts = {}) {
|
|
16958
17067
|
const surface = opts.surface ?? "local";
|
|
17068
|
+
const banner = bindingBanner(apiBaseUrl);
|
|
17069
|
+
const guidance = composeGuidance(banner, orgInstructions2);
|
|
16959
17070
|
const server2 = new McpServer({ name: "pipebrain", version: opts.version ?? DEFAULT_VERSION }, {
|
|
16960
|
-
//
|
|
16961
|
-
//
|
|
16962
|
-
//
|
|
16963
|
-
//
|
|
16964
|
-
//
|
|
16965
|
-
//
|
|
16966
|
-
|
|
17071
|
+
// A BOUNDED INDEX, not a document container (PIPE-D30). The client truncates this
|
|
17072
|
+
// field at 2KB and shares roughly 4KB across all configured servers, so what goes
|
|
17073
|
+
// here is: which package is speaking (only when the legacy wrapper arms the notice),
|
|
17074
|
+
// which environment this is bound to, the call that returns the binding guidance,
|
|
17075
|
+
// and the three rules whose violation cannot be undone. Everything else comes from
|
|
17076
|
+
// `docs/DOCTRINE.md` and the org's saved texts, on the work-context read.
|
|
17077
|
+
//
|
|
17078
|
+
// Plus, only when there is something to say, one line about the MACHINE: its shared
|
|
17079
|
+
// agent configuration is not the one this build ships (PIPE-354 AC-5). Read here
|
|
17080
|
+
// rather than passed in, because the fact is about the filesystem this process is
|
|
17081
|
+
// running on and there is no caller better placed to know it; a read failure
|
|
17082
|
+
// degrades to silence, since a session must not fail to start over an installer.
|
|
17083
|
+
instructions: composeIndex(successorNotice() + banner, machineNotice())
|
|
16967
17084
|
});
|
|
16968
17085
|
server2.registerTool("list_projects", {
|
|
16969
17086
|
description: "List all projects with their key, environments, milestones (token, label, active), and open-issue counts \u2014 the project switcher and workflow entry point (\xA77).",
|
|
@@ -16973,7 +17090,7 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
16973
17090
|
limit: external_exports.number().int().min(1).max(500).optional(),
|
|
16974
17091
|
offset: external_exports.number().int().nonnegative().optional()
|
|
16975
17092
|
}
|
|
16976
|
-
}, (args) => run(() => client2.listProjects(args)));
|
|
17093
|
+
}, (args) => run(() => client2.listProjects(args), GUIDANCE_POINTER));
|
|
16977
17094
|
server2.registerTool("create_project", {
|
|
16978
17095
|
description: "Create a new project \u2014 the workflow bootstrap entry point, since issues and requirements are all project-scoped and nothing can be filed until a project exists. Both `name` (its display label) and `key` are required; the key is 2\u20135 uppercase letters (A\u2013Z, e.g. `DWB`), unique per org, and becomes the prefix of every display id in the project. Returns the created project (\xA77).",
|
|
16979
17096
|
// Inline shape: @pipeline/domain has no standalone create-project schema — the
|
|
@@ -16992,21 +17109,57 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
16992
17109
|
// path that has no precedent anywhere on this surface. The viewer narrowing the
|
|
16993
17110
|
// web queue offers is filter state on that view, not something an agent picks.
|
|
16994
17111
|
}, () => (
|
|
16995
|
-
// `{ items }`, mirroring the HTTP envelope rather than unwrapping to a bare
|
|
16996
|
-
//
|
|
16997
|
-
//
|
|
16998
|
-
//
|
|
16999
|
-
|
|
17112
|
+
// `{ items }`, mirroring the HTTP envelope rather than unwrapping to a bare array
|
|
17113
|
+
// like the sibling list tools. The reason is the envelope itself: this tool is a
|
|
17114
|
+
// pass-through of one endpoint's answer, and reshaping it here would be a second
|
|
17115
|
+
// place for that answer's shape to be decided.
|
|
17116
|
+
//
|
|
17117
|
+
// The two surfaces are NOT interchangeable, and nothing here should suggest they
|
|
17118
|
+
// are. This tool takes no narrowing arguments and returns the org's WHOLE queue,
|
|
17119
|
+
// while the web view narrows what it DRAWS — by project, by category, and by viewer
|
|
17120
|
+
// scope. One endpoint decides what counts as a gate, so that definition cannot drift
|
|
17121
|
+
// between them; what an agent reads here is everything waiting, rather than whatever
|
|
17122
|
+
// the operator happens to be looking at.
|
|
17123
|
+
run(async () => ({ items: await client2.listWaiting() }), GUIDANCE_POINTER)
|
|
17000
17124
|
));
|
|
17001
|
-
server2.registerTool(
|
|
17002
|
-
|
|
17003
|
-
|
|
17004
|
-
|
|
17005
|
-
|
|
17006
|
-
|
|
17007
|
-
|
|
17008
|
-
|
|
17009
|
-
|
|
17125
|
+
server2.registerTool(
|
|
17126
|
+
"get_work_context",
|
|
17127
|
+
{
|
|
17128
|
+
// TRIMMED WHEN THE GUIDANCE MOVED HERE (PIPE-341, PIPE-D30). This description was
|
|
17129
|
+
// 1,922 characters against a 2KB cap that truncates silently, and it is now also the
|
|
17130
|
+
// one description loaded into EVERY session by `anthropic/alwaysLoad`. The clauses
|
|
17131
|
+
// that went were the ones arguing for the rule rather than stating it; what a caller
|
|
17132
|
+
// needs to predict a refusal all stayed.
|
|
17133
|
+
description: "Everything a session needs before it starts work, in ONE read \u2014 assembled by the API and returned verbatim, with the BINDING GUIDANCE appended after it: the product doctrine and this organization\u2019s own writing rules, in full. This is the one call that returns them; every other read only names it. CALL IT FIRST, before you write anything. Addressed by `issue` OR by `project`, never both and never neither: an issue names its own project, so the pair is refused rather than resolved for you (\xA77). Given an ISSUE: that issue\u2019s full detail \u2014 the same payload `get_issue` returns \u2014 PLUS the BODY TEXT of every requirement it links, at the revision the link is pinned at. That text is the round trip this removes: `get_issue` names which documents bind the work and at which revision, never what they SAY. Comments come CAPPED at the newest `commentLimit`, with `commentCount` \u2014 the issue\u2019s TRUE total \u2014 beside them; read the issue directly for the rest. Given a PROJECT: `available`, the work a session could START right now. It EXCLUDES finished and abandoned work (`done`, `killed`), work another person has claimed, and anything blocked \u2014 the status guard would refuse to start that. Your OWN claims stay, and so does unscheduled work carrying no milestone. The item form carries no `available`; the project form carries no item. BOTH forms also carry the project half: the release currently being worked (the active milestone) and the project\u2019s cross-cutting requirements WITH their text \u2014 they bind every epic automatically. READ-ONLY \u2014 this claims nothing, moves nothing and writes nothing.",
|
|
17134
|
+
// The WHOLE domain schema rather than its `.shape`: the exactly-one-of rule is a
|
|
17135
|
+
// refinement on the OBJECT, and a raw shape drops it — the SDK validates against
|
|
17136
|
+
// what it is handed, so the shape alone would quietly accept both arguments and
|
|
17137
|
+
// neither. Handing it the schema lets its refusal reach the caller verbatim,
|
|
17138
|
+
// naming the two arguments it refused rather than a reworded stand-in.
|
|
17139
|
+
inputSchema: getWorkContextInputSchema,
|
|
17140
|
+
// THE TWO ANNOTATIONS PIPE-D30 REQUIRES, and the reason each key is spelled out
|
|
17141
|
+
// rather than built: an unknown `_meta` key is IGNORED — no error, no warning — so a
|
|
17142
|
+
// typo leaves the tool silently un-annotated and nothing anywhere goes red.
|
|
17143
|
+
//
|
|
17144
|
+
// maxResultSizeChars — this tool's OWN result threshold, which applies for text
|
|
17145
|
+
// content independently of the operator's `MAX_MCP_OUTPUT_TOKENS`. Declared rather
|
|
17146
|
+
// than inherited: the default caps output at 25,000 tokens and the pack plus the
|
|
17147
|
+
// guidance can exceed that, and the default is a setting this server cannot see.
|
|
17148
|
+
//
|
|
17149
|
+
// alwaysLoad — tool search defers tool definitions until an agent searches for
|
|
17150
|
+
// them, and the one call a session is supposed to make FIRST must not require a
|
|
17151
|
+
// search step to become visible. It is the only tool here that gets it: every
|
|
17152
|
+
// upfront definition is context spent before the conversation starts.
|
|
17153
|
+
_meta: {
|
|
17154
|
+
"anthropic/maxResultSizeChars": WORK_CONTEXT_RESULT_CHARS,
|
|
17155
|
+
"anthropic/alwaysLoad": true
|
|
17156
|
+
}
|
|
17157
|
+
},
|
|
17158
|
+
// THE COMPOSED GUIDANCE RIDES THE RESULT, as a second text block after the API's JSON.
|
|
17159
|
+
// This is the delivery PIPE-D30 chose: a tool result is not subject to the 2KB cap that
|
|
17160
|
+
// cut the previous mechanism to a tenth of the document.
|
|
17161
|
+
(args) => run(() => client2.getWorkContext(args), guidance)
|
|
17162
|
+
);
|
|
17010
17163
|
server2.registerTool("create_milestone", {
|
|
17011
17164
|
description: "Create a milestone on a project. `token` is the short ref issues point at via their `milestone` field (e.g. `M1`) and is unique per project (a duplicate is rejected); `label` is its display name (e.g. `Feature Verification`). Optional `position` orders it (0-based, defaults to last); optional `active` marks it the current milestone \u2014 AT MOST ONE milestone per project is active, so `active: true` transactionally DEMOTES the previously active one rather than failing. Returns the created milestone (\xA77).",
|
|
17012
17165
|
inputSchema: {
|
|
@@ -17090,25 +17243,25 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17090
17243
|
// API-level default would strip out the very rows those chips exist to reveal.
|
|
17091
17244
|
// Only an OMITTED status is filled in — an explicit one, terminal values and
|
|
17092
17245
|
// all, passes through untouched.
|
|
17093
|
-
(args) => run(() => client2.listIssues(args.status === void 0 ? { ...args, status: [...OPEN_STATUSES] } : args))
|
|
17246
|
+
(args) => run(() => client2.listIssues(args.status === void 0 ? { ...args, status: [...OPEN_STATUSES] } : args), GUIDANCE_POINTER)
|
|
17094
17247
|
);
|
|
17095
17248
|
server2.registerTool("get_issue", {
|
|
17096
17249
|
description: "Full issue detail by display id or uuid: body, criteria (states, modes, evidence), explicit requirement links (pinned revision + staleness), the project\u2019s cross-cutting requirements at their latest approved revision, children, and comments (\xA77). Carries `terminal`: true when the issue is finished (`done`) or deliberately abandoned (`killed`) and is therefore NOT buildable work \u2014 check it before acting on an issue you were handed by key. Reading criterion state: `state_evidence` is required only when a TOKEN sets one, so a `manual` criterion \u2014 settable only from a web session \u2014 carries none by design, and the attribution the server already stores (who set it, and when) IS its record. What this payload tells you about ANOTHER issue goes stale. Reference that issue by key and say what you need from it; never restate its status, its criteria, or whether those criteria passed \u2014 such a sentence is true when written and wrong later, and a terminal issue is never revisited to correct it, so the stale reading is what the next reader inherits.",
|
|
17097
17250
|
inputSchema: { issue: issueRefSchema }
|
|
17098
|
-
}, (args) => run(() => client2.getIssue(args.issue)));
|
|
17251
|
+
}, (args) => run(() => client2.getIssue(args.issue), GUIDANCE_POINTER));
|
|
17099
17252
|
server2.registerTool("create_issue", {
|
|
17100
|
-
description: "Create an issue (epic/task/bug/feedback). Omitted status defaults to triage (still subject to \xA76 guards); omitted environment resolves explicit \u2192 parent\u2019s environment \u2192 project default (prod). Optional parent (self/cycle rejected), priority, milestone, requirement_links (pins each at its approved revision; a target with no approved revision is rejected), deliverable_links (content deliverables like DWB-C3/bsky \u2014 pins each at its current revision; the promotion path), and ordered {text, verification, howTo} criteria (\xA77). A `status` other than `triage` or `killed` REQUIRES `milestone` (PIPE-R10 rule 9): work past triage belongs to a release, and the create is refused (`milestone_required`) without one. A `manual` criterion must carry `howTo` \u2014 one or two sentences saying where to look, what to do, and what counts as passing; writing or changing one without it is refused (`how_to_required`), and so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug alike \u2014 with feedback the one exception, refused with `criteria_not_supported` because it is a report rather than agreed work; `promote_feedback` retypes it into work before anything is built. When a bug is filed against a defined acceptance criterion, pass that criterion\u2019s uuid as `criterionId` and the bug records which criterion it was filed against \u2014 the criterion then names what has already been reported (it arrives as `filedBugs` on every criteria read) beside the action that files again, so a duplicate is prevented by information rather than by removal. Filing a second bug against one criterion is a supported situation, not a mistake: one row can fail for two different reasons at once. Bugs only: any other type naming a criterion is refused (`criterion_requires_bug`). Writing guidance for this tool lives in the product doctrine
|
|
17253
|
+
description: "Create an issue (epic/task/bug/feedback). Omitted status defaults to triage (still subject to \xA76 guards); omitted environment resolves explicit \u2192 parent\u2019s environment \u2192 project default (prod). Optional parent (self/cycle rejected), priority, milestone, requirement_links (pins each at its approved revision; a target with no approved revision is rejected), deliverable_links (content deliverables like DWB-C3/bsky \u2014 pins each at its current revision; the promotion path), and ordered {text, verification, howTo} criteria (\xA77). A `status` other than `triage` or `killed` REQUIRES `milestone` (PIPE-R10 rule 9): work past triage belongs to a release, and the create is refused (`milestone_required`) without one. A `manual` criterion must carry `howTo` \u2014 one or two sentences saying where to look, what to do, and what counts as passing; writing or changing one without it is refused (`how_to_required`), and so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug alike \u2014 with feedback the one exception, refused with `criteria_not_supported` because it is a report rather than agreed work; `promote_feedback` retypes it into work before anything is built. When a bug is filed against a defined acceptance criterion, pass that criterion\u2019s uuid as `criterionId` and the bug records which criterion it was filed against \u2014 the criterion then names what has already been reported (it arrives as `filedBugs` on every criteria read) beside the action that files again, so a duplicate is prevented by information rather than by removal. Filing a second bug against one criterion is a supported situation, not a mistake: one row can fail for two different reasons at once. Bugs only: any other type naming a criterion is refused (`criterion_requires_bug`). Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CSearch before you create\u201D and \u201CThe bar for filing a bug\u201D.",
|
|
17101
17254
|
inputSchema: createIssueInputSchema.shape
|
|
17102
17255
|
}, (args) => run(() => client2.createIssue(args)));
|
|
17103
17256
|
server2.registerTool("promote_feedback", {
|
|
17104
|
-
description: "Promote a FEEDBACK issue into a NEW epic or task (PIPE-143): creates the issue in the feedback\u2019s project (`title` + `type` required; omitted environment inherits the FEEDBACK\u2019s environment, not the project default; optional status/body/priority/milestone/requirement_links/criteria as in create_issue), records the feedback as its permanent source (`promotedFrom`/`promotedTo` on both details), closes the feedback as done, and comments the new key on it. The feedback\u2019s own title/body are never touched \u2014 write the new issue\u2019s body as real work. Rejected (409) for non-feedback, terminal, or already-promoted sources. Returns the created issue. A `status` other than `triage` or `killed` REQUIRES `milestone` (PIPE-R10 rule 9), exactly as in create_issue; the SOURCE feedback needs none to be closed by the promotion. A `manual` criterion must carry `howTo` \u2014 one or two sentences saying where to look, what to do, and what counts as passing; writing or changing one without it is refused (`how_to_required`), and so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug alike \u2014 with feedback the one exception, refused with `criteria_not_supported` because it is a report rather than agreed work; this tool is what retypes it into work before anything is built. Writing guidance for this tool lives in the product doctrine
|
|
17257
|
+
description: "Promote a FEEDBACK issue into a NEW epic or task (PIPE-143): creates the issue in the feedback\u2019s project (`title` + `type` required; omitted environment inherits the FEEDBACK\u2019s environment, not the project default; optional status/body/priority/milestone/requirement_links/criteria as in create_issue), records the feedback as its permanent source (`promotedFrom`/`promotedTo` on both details), closes the feedback as done, and comments the new key on it. The feedback\u2019s own title/body are never touched \u2014 write the new issue\u2019s body as real work. Rejected (409) for non-feedback, terminal, or already-promoted sources. Returns the created issue. A `status` other than `triage` or `killed` REQUIRES `milestone` (PIPE-R10 rule 9), exactly as in create_issue; the SOURCE feedback needs none to be closed by the promotion. A `manual` criterion must carry `howTo` \u2014 one or two sentences saying where to look, what to do, and what counts as passing; writing or changing one without it is refused (`how_to_required`), and so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug alike \u2014 with feedback the one exception, refused with `criteria_not_supported` because it is a report rather than agreed work; this tool is what retypes it into work before anything is built. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CSearch before you create\u201D and \u201CThe bar for filing a bug\u201D.",
|
|
17105
17258
|
inputSchema: {
|
|
17106
17259
|
issue: external_exports.string().min(1),
|
|
17107
17260
|
...promoteFeedbackInputSchema.shape
|
|
17108
17261
|
}
|
|
17109
17262
|
}, ({ issue: issue2, ...fields }) => run(() => client2.promoteFeedback(issue2, fields)));
|
|
17110
17263
|
server2.registerTool("open_review", {
|
|
17111
|
-
description: "Open the COMPLETION REVIEW \u2014 the reading of an epic\u2019s WHOLE accumulated change as one body of work, by a third fresh session that neither built it nor evaluated it. It runs at one moment: once the epic\u2019s contract is green and nothing is left to check, and before anyone sets it `done`. It creates one ORDINARY `task` at `ready` carrying the review checklist as its body \u2014 no new record type. The epic form (`epic`) titles it `Completion review \u2014 <KEY-n> <epic title>`, files it as a CHILD of the epic and inherits the epic\u2019s milestone; the milestone form (`project` + `milestone`) titles it `Completion review \u2014 milestone <token>`, carries that token and has no parent \u2014 nothing here models a milestone as complete. Pass EXACTLY ONE of `epic` or `project` + `milestone`: both, or neither, is refused. One review at a time per scope \u2014 a second is refused (`review_already_open`) NAMING the standing one, so read, finish or kill that task instead \u2014 and a review opens only on an epic (`review_target_not_epic`). NO NEW GATE was added to `done`, and the whole mechanism depends on that: file each finding as ordinary work \u2014 a CHILD OF THE EPIC, a sibling of this review \u2014 and the children clause already on `done` holds the epic open until every finding is done or killed. A finding filed UNDER the review gates nothing, because that gate reads one level deep. Killing the review, or any finding, is the recorded waiver: deliberately ungated, and written to the status history with who did it and through which token. An epic that never opened a review still closes exactly as it did before. Writing guidance for the checklist and for each finding lives in the product doctrine
|
|
17264
|
+
description: "Open the COMPLETION REVIEW \u2014 the reading of an epic\u2019s WHOLE accumulated change as one body of work, by a third fresh session that neither built it nor evaluated it. It runs at one moment: once the epic\u2019s contract is green and nothing is left to check, and before anyone sets it `done`. It creates one ORDINARY `task` at `ready` carrying the review checklist as its body \u2014 no new record type. The epic form (`epic`) titles it `Completion review \u2014 <KEY-n> <epic title>`, files it as a CHILD of the epic and inherits the epic\u2019s milestone; the milestone form (`project` + `milestone`) titles it `Completion review \u2014 milestone <token>`, carries that token and has no parent \u2014 nothing here models a milestone as complete. Pass EXACTLY ONE of `epic` or `project` + `milestone`: both, or neither, is refused. One review at a time per scope \u2014 a second is refused (`review_already_open`) NAMING the standing one, so read, finish or kill that task instead \u2014 and a review opens only on an epic (`review_target_not_epic`). NO NEW GATE was added to `done`, and the whole mechanism depends on that: file each finding as ordinary work \u2014 a CHILD OF THE EPIC, a sibling of this review \u2014 and the children clause already on `done` holds the epic open until every finding is done or killed. A finding filed UNDER the review gates nothing, because that gate reads one level deep. Killing the review, or any finding, is the recorded waiver: deliberately ungated, and written to the status history with who did it and through which token. An epic that never opened a review still closes exactly as it did before. Writing guidance for the checklist and for each finding lives in the product doctrine, which `get_work_context` returns.",
|
|
17112
17265
|
// The WHOLE domain schema rather than its `.shape`, for `get_work_context`'s
|
|
17113
17266
|
// reason: the exactly-one-of rule is a refinement on the OBJECT, and a raw
|
|
17114
17267
|
// shape drops it — the refusal naming the two forms would never reach the
|
|
@@ -17116,7 +17269,7 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17116
17269
|
inputSchema: openReviewInputSchema
|
|
17117
17270
|
}, (args) => run(() => client2.openReview(args)));
|
|
17118
17271
|
server2.registerTool("update_issue", {
|
|
17119
|
-
description: "Update any subset of an issue\u2019s title, body, type, priority, milestone, environment, parent (self/cycle rejected), requirement_links (add/remove; adding a link to an unapproved requirement is rejected), deliverable_links (add/remove content-deliverable links, add pins at the current revision), or decision_links (add/remove decision records this work sits under \u2014 `KEY-D{n}`, from `list_decisions`). Changing type to `feedback` is rejected while the issue has criteria (\xA77), since feedback carries none; epic, task and bug interchange freely and the criteria ride along, keeping their AC-{n} numbers and their states. Clearing `milestone` (passing null) is rejected while the issue sits past `triage` \u2014 work already scheduled keeps a milestone (PIPE-R10 rule 9); swap it for a different one, or move the issue back to `triage` first. Writing guidance for this tool lives in the product doctrine
|
|
17272
|
+
description: "Update any subset of an issue\u2019s title, body, type, priority, milestone, environment, parent (self/cycle rejected), requirement_links (add/remove; adding a link to an unapproved requirement is rejected), deliverable_links (add/remove content-deliverable links, add pins at the current revision), or decision_links (add/remove decision records this work sits under \u2014 `KEY-D{n}`, from `list_decisions`). Changing type to `feedback` is rejected while the issue has criteria (\xA77), since feedback carries none; epic, task and bug interchange freely and the criteria ride along, keeping their AC-{n} numbers and their states. Clearing `milestone` (passing null) is rejected while the issue sits past `triage` \u2014 work already scheduled keeps a milestone (PIPE-R10 rule 9); swap it for a different one, or move the issue back to `triage` first. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CLink by key; never paste another record\u2019s text\u201D and \u201CA decision you need is filed as a question, never written into a body\u201D.",
|
|
17120
17273
|
inputSchema: updateIssueInputSchema.shape
|
|
17121
17274
|
}, ({ issue: issue2, ...fields }) => run(() => client2.updateIssue(issue2, fields)));
|
|
17122
17275
|
server2.registerTool("set_status", {
|
|
@@ -17132,11 +17285,11 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17132
17285
|
inputSchema: { issue: issueRefSchema }
|
|
17133
17286
|
}, (args) => run(() => client2.releaseIssue(args.issue)));
|
|
17134
17287
|
server2.registerTool("add_comment", {
|
|
17135
|
-
description: "Add a markdown comment to an issue, attributed to the operator (and token label over MCP). A comment EXPLAINS a blockage \u2014 the full account of what you hit and why you stopped \u2014 but never records one: `set_blocked` (with a reason) and `link_blocker` (when you are waiting on another issue) are what put an issue in the blocked state (\xA77/WORKFLOW). Writing guidance for a comment lives in the product doctrine
|
|
17288
|
+
description: "Add a markdown comment to an issue, attributed to the operator (and token label over MCP). A comment EXPLAINS a blockage \u2014 the full account of what you hit and why you stopped \u2014 but never records one: `set_blocked` (with a reason) and `link_blocker` (when you are waiting on another issue) are what put an issue in the blocked state (\xA77/WORKFLOW). Writing guidance for a comment lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CEvery loose end has one destination, and none of them is prose\u201D.",
|
|
17136
17289
|
inputSchema: commentInputSchema.shape
|
|
17137
17290
|
}, (args) => run(() => client2.addComment(args.issue, args.body)));
|
|
17138
17291
|
server2.registerTool("set_blocked", {
|
|
17139
|
-
description: "Record an issue as BLOCKED with a required human-readable `reason` \u2014 state on the issue, not a comment: it shows on the issue and counts on the project\u2019s Work page, and only clearing it (`clear_blocked`) ends it. Setting it again on an already-blocked issue replaces the standing reason and keeps the old one as history, attributed to whoever set it. The reason is one or two lines saying what the work is waiting on; the full account of what you tried still belongs in a comment. Returns the updated issue detail, whose `blocked` carries the reason and who set it. Writing guidance for the reason lives in the product doctrine
|
|
17292
|
+
description: "Record an issue as BLOCKED with a required human-readable `reason` \u2014 state on the issue, not a comment: it shows on the issue and counts on the project\u2019s Work page, and only clearing it (`clear_blocked`) ends it. Setting it again on an already-blocked issue replaces the standing reason and keeps the old one as history, attributed to whoever set it. The reason is one or two lines saying what the work is waiting on; the full account of what you tried still belongs in a comment. Returns the updated issue detail, whose `blocked` carries the reason and who set it. Writing guidance for the reason lives in the product doctrine, which `get_work_context` returns.",
|
|
17140
17293
|
inputSchema: setBlockedInputSchema.shape
|
|
17141
17294
|
}, (args) => run(() => client2.setBlocked(args.issue, args.reason)));
|
|
17142
17295
|
server2.registerTool("clear_blocked", {
|
|
@@ -17152,11 +17305,11 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17152
17305
|
inputSchema: linkBlockerInputSchema.shape
|
|
17153
17306
|
}, (args) => run(() => client2.removeBlocker(args.issue, args.blocker)));
|
|
17154
17307
|
server2.registerTool("set_criteria", {
|
|
17155
|
-
description: "Define or replace the ordered acceptance criteria list ({text, verification, howTo}) on a piece of work. Matched rows keep their stable AC-{n} number; new items get fresh never-reused numbers; removed items are soft-deleted. A matched row keeps its state only when BOTH its text and verification mode are unchanged \u2014 a mode flip resets it to pending (\xA77). A `manual` criterion must carry `howTo` \u2014 one or two sentences saying where to look, what to do, and what counts as passing; writing or changing one without it is refused (`how_to_required`), and so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug alike \u2014 with feedback the one exception, refused with `criteria_not_supported` because it is a report rather than agreed work; `promote_feedback` retypes it into work before anything is built. Raising a criterion from `manual` to `auto` both resets its state and clears the stored `howTo`, so a criterion moved to `auto` has lost its checking instructions \u2014 moving it back to `manual` does not restore them, and is refused (`how_to_required`) until you write new ones. Writing guidance for a criterion, and for choosing its mode, lives in the product doctrine
|
|
17308
|
+
description: "Define or replace the ordered acceptance criteria list ({text, verification, howTo}) on a piece of work. Matched rows keep their stable AC-{n} number; new items get fresh never-reused numbers; removed items are soft-deleted. A matched row keeps its state only when BOTH its text and verification mode are unchanged \u2014 a mode flip resets it to pending (\xA77). A `manual` criterion must carry `howTo` \u2014 one or two sentences saying where to look, what to do, and what counts as passing; writing or changing one without it is refused (`how_to_required`), and so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug alike \u2014 with feedback the one exception, refused with `criteria_not_supported` because it is a report rather than agreed work; `promote_feedback` retypes it into work before anything is built. Raising a criterion from `manual` to `auto` both resets its state and clears the stored `howTo`, so a criterion moved to `auto` has lost its checking instructions \u2014 moving it back to `manual` does not restore them, and is refused (`how_to_required`) until you write new ones. Writing guidance for a criterion, and for choosing its mode, lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CVerification modes follow what can be driven\u201D.",
|
|
17156
17309
|
inputSchema: setCriteriaInputSchema.shape
|
|
17157
17310
|
}, (args) => run(() => client2.setCriteria(args.issue, args.criteria)));
|
|
17158
17311
|
server2.registerTool("set_criterion_state", {
|
|
17159
|
-
description: "Set a criterion\u2019s state (pending/passed/failed) on whatever work carries it \u2014 an epic, a task and a bug alike \u2014 addressed by criterionId ALONE, or by issue + criterionNumber (the stable AC-{n}); exactly one form. Feedback never carries criteria, so neither address resolves to one. A token-authenticated call is REJECTED on a manual criterion (manual state is web-session-only, \xA74); evidence is required when a token marks passed/failed (\xA77). Writing guidance for that evidence lives in the product doctrine
|
|
17312
|
+
description: "Set a criterion\u2019s state (pending/passed/failed) on whatever work carries it \u2014 an epic, a task and a bug alike \u2014 addressed by criterionId ALONE, or by issue + criterionNumber (the stable AC-{n}); exactly one form. Feedback never carries criteria, so neither address resolves to one. A token-authenticated call is REJECTED on a manual criterion (manual state is web-session-only, \xA74); evidence is required when a token marks passed/failed (\xA77). Writing guidance for that evidence lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CVerification modes follow what can be driven\u201D.",
|
|
17160
17313
|
// `issue` is OPTIONAL: the criterionId-only form needs no issue ref. The
|
|
17161
17314
|
// exactly-one-of rule can't be expressed in a raw shape, so the handler
|
|
17162
17315
|
// enforces it via the shared `@pipeline/domain` refine (mirror of §7).
|
|
@@ -17187,7 +17340,7 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17187
17340
|
});
|
|
17188
17341
|
}));
|
|
17189
17342
|
server2.registerTool("ask_questions", {
|
|
17190
|
-
description: "Ask the operator a BATCH of questions on an issue \u2014 `questions` takes 1 or more {text, options?}, and a session with one open decision usually has two or three, so ask them in ONE call rather than one at a time (a half-asked set costs the operator a second pass). `options` are the choices you propose, in the order you offer them; omit it to ask in open words. Each question gets a stable per-issue `Q-{n}` number that is NEVER reused. Returns the updated issue detail, and every issue read from then on carries `questions` with their answers \u2014 so read the answer in the `get_issue` you were going to make anyway, and READ the existing questions and answers BEFORE asking again rather than re-asking a decision already taken. Answering is reserved for a person in the web UI: there is deliberately no tool for it on this surface (PIPE-R22 rule 4). A question is withdrawn with `withdraw_question`. Writing guidance for a question lives in the product doctrine
|
|
17343
|
+
description: "Ask the operator a BATCH of questions on an issue \u2014 `questions` takes 1 or more {text, options?}, and a session with one open decision usually has two or three, so ask them in ONE call rather than one at a time (a half-asked set costs the operator a second pass). `options` are the choices you propose, in the order you offer them; omit it to ask in open words. Each question gets a stable per-issue `Q-{n}` number that is NEVER reused. Returns the updated issue detail, and every issue read from then on carries `questions` with their answers \u2014 so read the answer in the `get_issue` you were going to make anyway, and READ the existing questions and answers BEFORE asking again rather than re-asking a decision already taken. Answering is reserved for a person in the web UI: there is deliberately no tool for it on this surface (PIPE-R22 rule 4). A question is withdrawn with `withdraw_question`. Writing guidance for a question lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CA decision you need is filed as a question, never written into a body\u201D.",
|
|
17191
17344
|
inputSchema: askQuestionsInputSchema.shape
|
|
17192
17345
|
}, (args) => run(() => client2.askQuestions(args.issue, args.questions)));
|
|
17193
17346
|
server2.registerTool("withdraw_question", {
|
|
@@ -17197,17 +17350,17 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17197
17350
|
server2.registerTool("list_requirements", {
|
|
17198
17351
|
description: "List requirements filtered by project and text: display id, title, summary (the latest revision\u2019s one/two-line outline description), area (product-area label), cross_cutting flag, lifecycle (draft/approved/amended), latest + approved revision numbers, and linked-epic count (\xA77).",
|
|
17199
17352
|
inputSchema: listRequirementsFilterSchema.shape
|
|
17200
|
-
}, (args) => run(() => client2.listRequirements(args)));
|
|
17353
|
+
}, (args) => run(() => client2.listRequirements(args), GUIDANCE_POINTER));
|
|
17201
17354
|
server2.registerTool("get_requirement", {
|
|
17202
17355
|
description: "Get a requirement by display id or uuid. Defaults to the approved revision (latest if never approved) unless `revision` is given; includes lifecycle, revision list, approvals, and linked issues (\xA77).",
|
|
17203
17356
|
inputSchema: { requirement: requirementRefSchema, revision: positiveIntSchema.optional() }
|
|
17204
|
-
}, (args) => run(() => client2.getRequirement(args.requirement, args.revision)));
|
|
17357
|
+
}, (args) => run(() => client2.getRequirement(args.requirement, args.revision), GUIDANCE_POINTER));
|
|
17205
17358
|
server2.registerTool("create_requirement", {
|
|
17206
|
-
description: 'Create a project-scoped requirement document with revision 1 (title, body). Optional summary is a short one/two-line outline description (versioned on the revision, like title/body); optional area is a free-text product-area label ("Backup", "Billing") used to group/filter the outline (metadata, like cross_cutting). Optional cross_cutting flags it as binding every epic in the project automatically \u2014 never link cross-cutting documents (\xA75 rule 6, \xA77). Writing guidance for this tool lives in the product doctrine
|
|
17359
|
+
description: 'Create a project-scoped requirement document with revision 1 (title, body). Optional summary is a short one/two-line outline description (versioned on the revision, like title/body); optional area is a free-text product-area label ("Backup", "Billing") used to group/filter the outline (metadata, like cross_cutting). Optional cross_cutting flags it as binding every epic in the project automatically \u2014 never link cross-cutting documents (\xA75 rule 6, \xA77). Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CSearch before you create\u201D and \u201CLink by key; never paste another record\u2019s text\u201D.',
|
|
17207
17360
|
inputSchema: createRequirementInputSchema.shape
|
|
17208
17361
|
}, (args) => run(() => client2.createRequirement(args)));
|
|
17209
17362
|
server2.registerTool("update_requirement", {
|
|
17210
|
-
description: "Update a requirement: title, body, and/or summary APPENDS a new revision (nothing is edited in place; summary versions like title/body); optional note records why. Optional area (product-area label), cross_cutting, and decision_links (add/remove decision records this rule sits under \u2014 `KEY-D{n}`, from `list_decisions`) are metadata changes (no revision) \u2014 toggling cross_cutting ON is rejected while explicit links exist; unlink first. Supplying none of title/body/summary/area/cross_cutting/decision_links is an empty edit (400) (\xA77). Writing guidance for this tool lives in the product doctrine
|
|
17363
|
+
description: "Update a requirement: title, body, and/or summary APPENDS a new revision (nothing is edited in place; summary versions like title/body); optional note records why. Optional area (product-area label), cross_cutting, and decision_links (add/remove decision records this rule sits under \u2014 `KEY-D{n}`, from `list_decisions`) are metadata changes (no revision) \u2014 toggling cross_cutting ON is rejected while explicit links exist; unlink first. Supplying none of title/body/summary/area/cross_cutting/decision_links is an empty edit (400) (\xA77). Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CLink by key; never paste another record\u2019s text\u201D.",
|
|
17211
17364
|
// Inline shape (updateRequirementInputSchema is a refine, no .shape) — add
|
|
17212
17365
|
// summary/area here explicitly (§210) and decisionLinks (PIPE-84 §6); the
|
|
17213
17366
|
// empty-edit refine is enforced by the API, not this shape.
|
|
@@ -17227,17 +17380,17 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17227
17380
|
inputSchema: approveRequirementInputSchema.shape
|
|
17228
17381
|
}, (args) => run(() => client2.approveRequirement(args.requirement, args.revision)));
|
|
17229
17382
|
server2.registerTool("create_decision", {
|
|
17230
|
-
description: "Record ONE decision as a project-scoped decision record (`KEY-D{n}`): `title` names the decision and `body` says what was decided and why. A new record starts `proposed`; moving it to `accepted` IS the acceptance \u2014 there is no separate approval step. Returns the created record. Title and body can be corrected only while the record is `proposed`; from `accepted` onward it is frozen, and the way to change a decision is to write a NEW record and `supersede_decision` the old one \u2014 which leaves the original readable and says where the current decision went. Nothing on any surface deletes one. Writing guidance for this tool lives in the product doctrine
|
|
17383
|
+
description: "Record ONE decision as a project-scoped decision record (`KEY-D{n}`): `title` names the decision and `body` says what was decided and why. A new record starts `proposed`; moving it to `accepted` IS the acceptance \u2014 there is no separate approval step. Returns the created record. Title and body can be corrected only while the record is `proposed`; from `accepted` onward it is frozen, and the way to change a decision is to write a NEW record and `supersede_decision` the old one \u2014 which leaves the original readable and says where the current decision went. Nothing on any surface deletes one. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CA decision record is written once\u201D and \u201CSearch before you create\u201D.",
|
|
17231
17384
|
inputSchema: createDecisionInputSchema.shape
|
|
17232
17385
|
}, (args) => run(() => client2.createDecision(args)));
|
|
17233
17386
|
server2.registerTool("get_decision", {
|
|
17234
17387
|
description: "Get a decision record by display id (`KEY-D{n}`) or uuid: title, body, lifecycle state (`proposed`/`accepted`/`superseded`), who accepted it and when, the record that replaced it, where the CURRENT decision lives when this one has been superseded, and everything that references it \u2014 every requirement and every work item, gathered from both directions.",
|
|
17235
17388
|
inputSchema: getDecisionInputSchema.shape
|
|
17236
|
-
}, (args) => run(() => client2.getDecision(args.decision)));
|
|
17389
|
+
}, (args) => run(() => client2.getDecision(args.decision), GUIDANCE_POINTER));
|
|
17237
17390
|
server2.registerTool("list_decisions", {
|
|
17238
17391
|
description: "List decision records filtered by `project` and `text`: display id, title, lifecycle state, the replacement when superseded, and how many requirements and work items cite each one. `text` matches over TITLE AND BODY, so search on what a decision was about rather than on what someone chose to call it.",
|
|
17239
17392
|
inputSchema: listDecisionsFilterSchema.shape
|
|
17240
|
-
}, (args) => run(() => client2.listDecisions(args)));
|
|
17393
|
+
}, (args) => run(() => client2.listDecisions(args), GUIDANCE_POINTER));
|
|
17241
17394
|
server2.registerTool("supersede_decision", {
|
|
17242
17395
|
description: "Replace an ACCEPTED decision record with another one: `decision` moves to `superseded` and names `replacedBy` as where the current decision now lives, so whoever opens the old record is sent to the standing one. This is the only way a decision changes \u2014 the original is never edited and never removed, and the whole chain stays readable. Refused when the record is still `proposed` (nothing has been decided yet, so correct it instead), when a record is named as its own replacement, when the replacement belongs to another project, or when the replacement\u2019s own chain already leads back to this record. Returns the superseded record.",
|
|
17243
17396
|
inputSchema: supersedeDecisionInputSchema.shape
|
|
@@ -17245,25 +17398,25 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17245
17398
|
server2.registerTool("list_content", {
|
|
17246
17399
|
description: "List content drafts filtered by project, text, area, and author: display id (KEY-C{n}), title, summary (latest revision), area, author, latest revision number, deliverable count, and linked-issue count. Content is a project-scoped sibling of requirements \u2014 raw material that gets promoted into work, not an issue type.",
|
|
17247
17400
|
inputSchema: listContentFilterSchema.shape
|
|
17248
|
-
}, (args) => run(() => client2.listContent(args)));
|
|
17401
|
+
}, (args) => run(() => client2.listContent(args), GUIDANCE_POINTER));
|
|
17249
17402
|
server2.registerTool("get_content", {
|
|
17250
17403
|
description: "Get a content draft by display id (KEY-C{n}) or uuid: its revision history (default latest, or `revision` to pin) plus every per-platform deliverable with its own revisions, posted metadata (url/postedAt), linked issues (with staleness), and linked assets.",
|
|
17251
17404
|
inputSchema: { content: contentRefSchema, revision: positiveIntSchema.optional() }
|
|
17252
|
-
}, (args) => run(() => client2.getContent(args.content, args.revision)));
|
|
17405
|
+
}, (args) => run(() => client2.getContent(args.content, args.revision), GUIDANCE_POINTER));
|
|
17253
17406
|
server2.registerTool("create_content", {
|
|
17254
|
-
description: 'Create a project-scoped content draft with revision 1 (title, body). Optional summary is a short outline description (versioned on the revision, like title/body); optional area is a free-text product-area label (metadata). Optional author is the editorial byline the draft is credited to (free text, e.g. "jason") \u2014 distinct from the acting operator, and the field the list filters by. Optional metadata is an open bag for capture provenance (tags, score, sensitivity, source refs) \u2014 stored passively, never acted on. This is the capture-sink target: a captured topic becomes a draft here. Writing guidance for this tool lives in the product doctrine
|
|
17407
|
+
description: 'Create a project-scoped content draft with revision 1 (title, body). Optional summary is a short outline description (versioned on the revision, like title/body); optional area is a free-text product-area label (metadata). Optional author is the editorial byline the draft is credited to (free text, e.g. "jason") \u2014 distinct from the acting operator, and the field the list filters by. Optional metadata is an open bag for capture provenance (tags, score, sensitivity, source refs) \u2014 stored passively, never acted on. This is the capture-sink target: a captured topic becomes a draft here. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CSearch before you create\u201D.',
|
|
17255
17408
|
inputSchema: createContentInputSchema.shape
|
|
17256
17409
|
}, (args) => run(() => client2.createContent(args)));
|
|
17257
17410
|
server2.registerTool("update_content", {
|
|
17258
|
-
description: "Update a content draft: title, body, and/or summary APPENDS a new revision (nothing is edited in place); optional note records why. Optional area, author, and metadata are metadata changes (no revision). Supplying none of title/body/summary/area/author/metadata is an empty edit (400). Writing guidance for this tool lives in the product doctrine
|
|
17411
|
+
description: "Update a content draft: title, body, and/or summary APPENDS a new revision (nothing is edited in place); optional note records why. Optional area, author, and metadata are metadata changes (no revision). Supplying none of title/body/summary/area/author/metadata is an empty edit (400). Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns.",
|
|
17259
17412
|
inputSchema: updateContentInputSchema.shape
|
|
17260
17413
|
}, ({ content, ...fields }) => run(() => client2.updateContent(content, fields)));
|
|
17261
17414
|
server2.registerTool("create_deliverable", {
|
|
17262
|
-
description: "Add a per-platform deliverable to a draft with revision 1 (the post text). `platform` is a slug configured for the project \u2014 one of its ACTIVE deliverable targets (see list_projects \u2192 deliverableTargets; defaults are bsky/x/reddit/hn/blog, but projects can add/remove them); an unconfigured slug is rejected. Optional body seeds revision 1. There is one deliverable per platform per draft \u2014 a variant is a revision (update_deliverable), not a second deliverable. Rendered id is DRAFT-KEY/platform (e.g. DWB-C3/bsky). Writing guidance for the post text lives in the product doctrine
|
|
17415
|
+
description: "Add a per-platform deliverable to a draft with revision 1 (the post text). `platform` is a slug configured for the project \u2014 one of its ACTIVE deliverable targets (see list_projects \u2192 deliverableTargets; defaults are bsky/x/reddit/hn/blog, but projects can add/remove them); an unconfigured slug is rejected. Optional body seeds revision 1. There is one deliverable per platform per draft \u2014 a variant is a revision (update_deliverable), not a second deliverable. Rendered id is DRAFT-KEY/platform (e.g. DWB-C3/bsky). Writing guidance for the post text lives in the product doctrine, which `get_work_context` returns.",
|
|
17263
17416
|
inputSchema: createDeliverableInputSchema.shape
|
|
17264
17417
|
}, ({ content, ...fields }) => run(() => client2.createDeliverable(content, fields)));
|
|
17265
17418
|
server2.registerTool("update_deliverable", {
|
|
17266
|
-
description: "Append a new revision (post text) to a deliverable, addressed as DRAFT-KEY/platform (e.g. DWB-C3/bsky); optional note records why. Platform identity is immutable \u2014 this only versions the post text. Writing guidance for the post text lives in the product doctrine
|
|
17419
|
+
description: "Append a new revision (post text) to a deliverable, addressed as DRAFT-KEY/platform (e.g. DWB-C3/bsky); optional note records why. Platform identity is immutable \u2014 this only versions the post text. Writing guidance for the post text lives in the product doctrine, which `get_work_context` returns.",
|
|
17267
17420
|
inputSchema: updateDeliverableInputSchema.shape
|
|
17268
17421
|
}, ({ deliverable, ...fields }) => run(() => client2.updateDeliverable(deliverable, fields)));
|
|
17269
17422
|
server2.registerTool("record_post", {
|
|
@@ -17271,7 +17424,7 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17271
17424
|
inputSchema: recordPostInputSchema.shape
|
|
17272
17425
|
}, ({ deliverable, ...fields }) => run(() => client2.recordPost(deliverable, fields)));
|
|
17273
17426
|
server2.registerTool("promote_deliverable", {
|
|
17274
|
-
description: "Promote a deliverable (DRAFT-KEY/platform, e.g. DWB-C3/bsky) to a NEW issue: creates the issue (`title` + `type` required; optional `status` defaults to triage; optional `body` seeds it \u2014 typically the current post text) AND a PROTECTED link pinning the deliverable\u2019s current revision. That link CANNOT be unlinked \u2014 the issue exists to ship this deliverable. (To link an EXISTING issue instead, use update_issue.deliverable_links, which stays removable.) Returns the created issue. Seeding the body with the deliverable\u2019s own text is the exception to linking rather than pasting \u2014 it is the point of this tool, and the protected link pins the revision that text came from. Writing guidance for this tool lives in the product doctrine
|
|
17427
|
+
description: "Promote a deliverable (DRAFT-KEY/platform, e.g. DWB-C3/bsky) to a NEW issue: creates the issue (`title` + `type` required; optional `status` defaults to triage; optional `body` seeds it \u2014 typically the current post text) AND a PROTECTED link pinning the deliverable\u2019s current revision. That link CANNOT be unlinked \u2014 the issue exists to ship this deliverable. (To link an EXISTING issue instead, use update_issue.deliverable_links, which stays removable.) Returns the created issue. Seeding the body with the deliverable\u2019s own text is the exception to linking rather than pasting \u2014 it is the point of this tool, and the protected link pins the revision that text came from. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CSearch before you create\u201D and \u201CLink by key; never paste another record\u2019s text\u201D.",
|
|
17275
17428
|
inputSchema: {
|
|
17276
17429
|
deliverable: external_exports.string().min(1),
|
|
17277
17430
|
title: external_exports.string().min(1),
|
|
@@ -17283,11 +17436,11 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17283
17436
|
server2.registerTool("list_assets", {
|
|
17284
17437
|
description: "List project assets (S3-backed files of any type) filtered by project, status (pending/active/rejected), category (image/video/document), and filename text, sorted by date or size (asc/desc): id, filename, contentType, size, sha256, status, hasThumbnail, and linkCount (live references). Metadata only \u2014 the bytes move by path, never through this result.",
|
|
17285
17438
|
inputSchema: listAssetsFilterSchema.shape
|
|
17286
|
-
}, (args) => run(() => client2.listAssets(args)));
|
|
17439
|
+
}, (args) => run(() => client2.listAssets(args), GUIDANCE_POINTER));
|
|
17287
17440
|
server2.registerTool("get_asset", {
|
|
17288
17441
|
description: "Get an asset\u2019s metadata by uuid: filename, contentType, size, sha256 (client-computed, server-verified by read-back), status, hasThumbnail, linkCount (live references), and timestamps. Metadata only \u2014 no value/URL is returned over MCP. To get the bytes, use download_asset (a LOCAL server only \u2014 it writes to a path on the machine running the server) or `ppb assets download`.",
|
|
17289
17442
|
inputSchema: { asset: uuidSchema }
|
|
17290
|
-
}, (args) => run(() => client2.getAsset(args.asset)));
|
|
17443
|
+
}, (args) => run(() => client2.getAsset(args.asset), GUIDANCE_POINTER));
|
|
17291
17444
|
if (surface === "local") {
|
|
17292
17445
|
server2.registerTool("upload_asset", {
|
|
17293
17446
|
description: "Upload a LOCAL FILE (by path) as a project asset and activate it: reads and SHA-256-hashes the file locally, registers the asset, PUTs the bytes directly to storage, and completes (server verifies the digest by read-back). Returns the asset metadata (status active on success). Only the file PATH crosses the wire \u2014 the bytes never do; do NOT pass base64 or file contents. contentType is guessed from the extension when omitted. Then use link_asset to attach the returned asset to a deliverable revision or a content draft.",
|
|
@@ -17303,11 +17456,11 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17303
17456
|
inputSchema: linkAssetInputSchema.shape
|
|
17304
17457
|
}, (args) => run(() => client2.linkAsset(args)));
|
|
17305
17458
|
server2.registerTool("unlink_asset", {
|
|
17306
|
-
description: "Remove an asset reference by its link id (soft-delete). The asset itself is untouched \u2014 this only drops the reference from its target.",
|
|
17459
|
+
description: "Remove an asset reference by its link id (soft-delete). The asset itself is untouched \u2014 this only drops the reference from its target. A link whose target is a CHAT MESSAGE is author-only: anyone but the person who wrote that message is refused with `not_message_author` (403), so an attachment you did not post is not yours to detach. Every other target type is removable by any member.",
|
|
17307
17460
|
inputSchema: unlinkAssetInputSchema.shape
|
|
17308
17461
|
}, (args) => run(() => client2.unlinkAsset(args.linkId)));
|
|
17309
17462
|
server2.registerTool("set_secret", {
|
|
17310
|
-
description: "Create a secret or add a new version under a project + environment (metadata only \u2014 the response NEVER echoes the value). `name` is UPPER_SNAKE_CASE (e.g. API_KEY); `environment` is the environment slug (dev/prod); optional `description`.",
|
|
17463
|
+
description: "Create a secret or add a new version under a project + environment (metadata only \u2014 the response NEVER echoes the value). `name` is UPPER_SNAKE_CASE (e.g. API_KEY); `environment` is the environment slug (dev/prod); optional `description`. Writing guidance for that description lives in the product doctrine, which `get_work_context` returns.",
|
|
17311
17464
|
inputSchema: {
|
|
17312
17465
|
project: projectKeySchema,
|
|
17313
17466
|
environment: external_exports.string().min(1),
|
|
@@ -17322,7 +17475,7 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17322
17475
|
project: projectKeySchema,
|
|
17323
17476
|
environment: external_exports.string().min(1).optional()
|
|
17324
17477
|
}
|
|
17325
|
-
}, ({ project, environment }) => run(() => client2.listSecrets(project, { env: environment })));
|
|
17478
|
+
}, ({ project, environment }) => run(() => client2.listSecrets(project, { env: environment }), GUIDANCE_POINTER));
|
|
17326
17479
|
server2.registerTool("describe_secret", {
|
|
17327
17480
|
description: "Describe one secret: its metadata plus its version history (version, created_at, created_by, and whether each version has been crypto-shredded). METADATA ONLY \u2014 no value is ever returned.",
|
|
17328
17481
|
inputSchema: {
|
|
@@ -17330,7 +17483,7 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17330
17483
|
environment: external_exports.string().min(1),
|
|
17331
17484
|
name: secretNameSchema
|
|
17332
17485
|
}
|
|
17333
|
-
}, ({ project, environment, name }) => run(() => client2.describeSecret(project, name, { env: environment })));
|
|
17486
|
+
}, ({ project, environment, name }) => run(() => client2.describeSecret(project, name, { env: environment }), GUIDANCE_POINTER));
|
|
17334
17487
|
server2.registerTool("rotate_secret", {
|
|
17335
17488
|
description: "Rotate an EXISTING secret: store a NEW version and advance the pointer (metadata only \u2014 the response NEVER echoes the value).",
|
|
17336
17489
|
inputSchema: {
|
|
@@ -17341,7 +17494,7 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17341
17494
|
}
|
|
17342
17495
|
}, ({ project, environment, name, value }) => run(() => client2.rotateSecret(project, name, { env: environment, value })));
|
|
17343
17496
|
server2.registerTool("update_secret_description", {
|
|
17344
|
-
description: "Update a secret's description (metadata-only edit \u2014 does not change the value or add a version). Writing guidance for that description lives in the product doctrine
|
|
17497
|
+
description: "Update a secret's description (metadata-only edit \u2014 does not change the value or add a version). Writing guidance for that description lives in the product doctrine, which `get_work_context` returns.",
|
|
17345
17498
|
inputSchema: {
|
|
17346
17499
|
project: projectKeySchema,
|
|
17347
17500
|
environment: external_exports.string().min(1),
|
|
@@ -17361,10 +17514,10 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
|
|
|
17361
17514
|
return { deleted: true, project, environment, name };
|
|
17362
17515
|
}));
|
|
17363
17516
|
server2.registerTool("list_org_instructions", {
|
|
17364
|
-
description: "List the org\u2019s agent instructions \u2014 the per-scope texts this server
|
|
17365
|
-
}, () => run(() => client2.listOrgInstructions()));
|
|
17517
|
+
description: "List the org\u2019s agent instructions \u2014 the per-scope texts this server composes into the guidance `get_work_context` returns, each under a heading naming its scope. Edits apply to NEW sessions only; use this to see the CURRENT saved values mid-session (at most one row per scope)."
|
|
17518
|
+
}, () => run(() => client2.listOrgInstructions(), GUIDANCE_POINTER));
|
|
17366
17519
|
server2.registerTool("set_org_instruction", {
|
|
17367
|
-
description: "Set (create or replace) one scope\u2019s org-level agent instruction text, max 4000 characters \u2014 over-cap is rejected, never truncated. Scopes: `general` (server-wide), `requirements`, `content`, and `work.<type>` (one per issue type, e.g. `work.epic`). Requires the ADMIN role in the token\u2019s org. Takes effect at the NEXT session start \u2014 running sessions keep the text they launched with. Returns the saved instruction. Writing guidance for an instruction body lives in the product doctrine
|
|
17520
|
+
description: "Set (create or replace) one scope\u2019s org-level agent instruction text, max 4000 characters \u2014 over-cap is rejected, never truncated. Scopes: `general` (server-wide), `requirements`, `content`, and `work.<type>` (one per issue type, e.g. `work.epic`). Requires the ADMIN role in the token\u2019s org. Takes effect at the NEXT session start \u2014 running sessions keep the text they launched with. Returns the saved instruction. Writing guidance for an instruction body lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CSetting an instruction replaces the whole body\u201D.",
|
|
17368
17521
|
inputSchema: setOrgInstructionInputSchema.shape
|
|
17369
17522
|
}, (args) => run(() => client2.setOrgInstruction(args.scope, args.body)));
|
|
17370
17523
|
server2.registerTool("delete_org_instruction", {
|
|
@@ -17436,7 +17589,7 @@ function loadMcpConfig() {
|
|
|
17436
17589
|
}
|
|
17437
17590
|
|
|
17438
17591
|
// src/version.ts
|
|
17439
|
-
var VERSION = "0.
|
|
17592
|
+
var VERSION = "0.33.0";
|
|
17440
17593
|
|
|
17441
17594
|
// src/main.ts
|
|
17442
17595
|
var config2 = loadMcpConfig();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pipebrain-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pipebrain MCP server for Claude Code and Claude Desktop
|
|
3
|
+
"version": "0.33.0",
|
|
4
|
+
"description": "Pipebrain MCP server for Claude Code and Claude Desktop \u2014 issues, requirements, content, assets, and metadata-only secrets over the Pipebrain API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "./dist/main.js",
|