@nonbot/mcp 0.1.0 → 0.2.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.
- package/README.md +9 -2
- package/dist/core/config.d.ts +1 -0
- package/dist/core/ipShape.d.ts +7 -0
- package/dist/core/ipShape.js +45 -0
- package/dist/core/layout.d.ts +38 -0
- package/dist/core/layout.js +193 -0
- package/dist/core/server.d.ts +1 -1
- package/dist/core/server.js +9 -4
- package/dist/core/toolsets/build.d.ts +78 -0
- package/dist/core/toolsets/build.js +367 -0
- package/dist/core/toolsets/ip.js +2 -2
- package/dist/core/toolsets/portfolio.js +4 -4
- package/dist/stdio.js +23 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ Ask your agent to "list my non.bot portfolios", "show what rules apply before I
|
|
|
100
100
|
## Tools
|
|
101
101
|
|
|
102
102
|
<!-- tool-table:start — generated by `npm run gen:tools`; tests/readme-tool-table.test.ts fails if this drifts from the code -->
|
|
103
|
-
### Portfolio (
|
|
103
|
+
### Portfolio (24)
|
|
104
104
|
|
|
105
105
|
The SAFe portfolio canvas: epics, features, stories, snapshots, personas, and the Run launch surface.
|
|
106
106
|
|
|
@@ -124,13 +124,20 @@ The SAFe portfolio canvas: epics, features, stories, snapshots, personas, and th
|
|
|
124
124
|
| `portfolio_create_activation` | Create an activation (a ▶ Run launch request) for a story. | write | `portfolio:write` + `runs:fire` |
|
|
125
125
|
| `portfolio_update_activation_status` | Transition an activation: pending → running \| failed \| cancelled. | write | `portfolio:write` |
|
|
126
126
|
| `portfolio_snapshot` | Save a labeled version snapshot of a portfolio’s CURRENT graph — a time-travel restore point. | write | `portfolio:write` |
|
|
127
|
+
| `portfolio_create` | Create a new, empty portfolio owned by the caller and return its id + version. | write | `portfolio:write` |
|
|
128
|
+
| `portfolio_add_items` | Add up to 200 items of any level (objective, key-result, epic, feature, story, enabler, milestone) in one versioned write. | write | `portfolio:write` |
|
|
129
|
+
| `portfolio_patch_item` | Change fields of ONE portfolio item without rewriting the graph (PATCH /api/portfolio/graph/node): `set` writes allowlisted PortfolioItemData fields (title, de… | write | `portfolio:write` |
|
|
130
|
+
| `portfolio_layout` | Re-position every item so the Timeline reads cleanly: x from each item's start date (the canvas reads dates off x), y top-down — objectives, key results, then… | write | `portfolio:write` |
|
|
131
|
+
| `portfolio_set_meta` | Graph-level settings that travel with the portfolio (the `__graph_meta__` record): `context_text` (the portfolio's context notes — what a newcomer reads first)… | write | `portfolio:write` |
|
|
132
|
+
| `portfolio_asset_upload` | Store one image (PNG / JPEG / WebP / GIF, ≤ 2.2 MB — downscale first; the app keeps images at 1024 px JPEG) in your private asset store and get back its URL, f… | write | `portfolio:write` |
|
|
127
133
|
|
|
128
|
-
### IP node (
|
|
134
|
+
### IP node (11)
|
|
129
135
|
|
|
130
136
|
Patent and trademark disclosures on the canvas: read the bag, edit it as an op list, add server-sealed evidence, export the hash list.
|
|
131
137
|
|
|
132
138
|
| Tool | What it does | Kind | PAT scope |
|
|
133
139
|
|---|---|---|---|
|
|
140
|
+
| `ip_create` | Add an IP node — a patent or trademark disclosure — to a portfolio, wired to its source items (the cards the disclosure is prepared FROM: their title, descript… | write | `portfolio:write` |
|
|
134
141
|
| `ip_list` | Every IP node (patent or trademark disclosure) in a portfolio: id, title, track, kind, started, live source count, live evidence count, filing date. | read | `portfolio:read` |
|
|
135
142
|
| `ip_get` | One IP node's full disclosure bag (units with status, value, provenance; evidence incl. tombstones; events; filing) plus the graph version to pass as expected_… | read | `portfolio:read` |
|
|
136
143
|
| `ip_evidence_list` | Evidence rows from the saved graph, tombstones included, with a seal verdict per row (true = sealed by this server, false = not, null = no signing key) and a w… | read | `portfolio:read` |
|
package/dist/core/config.d.ts
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type IpTrack = 'patent' | 'trademark';
|
|
2
|
+
export declare const IP_KINDS: Record<IpTrack, string[]>;
|
|
3
|
+
export declare const IP_NODE_SIZE: {
|
|
4
|
+
readonly width: 340;
|
|
5
|
+
readonly height: 184;
|
|
6
|
+
};
|
|
7
|
+
export declare function emptyIp(track: IpTrack, kind?: string): Record<string, unknown>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const UNIT_SHAPES = {
|
|
2
|
+
patent: [
|
|
3
|
+
{ id: 'u1', editor: 'text' }, { id: 'u2', editor: 'textarea' }, { id: 'u3', editor: 'steps' }, { id: 'u4', editor: 'proposals' },
|
|
4
|
+
{ id: 'u5', editor: 'people' }, { id: 'u6', editor: 'where' }, { id: 'u7', editor: 'events' }, { id: 'u8', editor: 'date' },
|
|
5
|
+
{ id: 'u9', editor: 'list' }, { id: 'u10', editor: 'figures' }, { id: 'u11', editor: 'owner' }, { id: 'u12', editor: 'text' },
|
|
6
|
+
],
|
|
7
|
+
trademark: [
|
|
8
|
+
{ id: 't1', editor: 'tm-mark' }, { id: 't2', editor: 'tm-type' }, { id: 't3', editor: 'tm-owner' }, { id: 't4', editor: 'tm-classes' },
|
|
9
|
+
{ id: 't5', editor: 'tm-basis' }, { id: 't6', editor: 'tm-specimen' }, { id: 't7', editor: 'tm-signatory' }, { id: 't8', editor: 'tm-search' },
|
|
10
|
+
],
|
|
11
|
+
};
|
|
12
|
+
export const IP_KINDS = { patent: ['provisional', 'nonprovisional', 'design'], trademark: ['application'] };
|
|
13
|
+
export const IP_NODE_SIZE = { width: 340, height: 184 };
|
|
14
|
+
const TM_EMPTY = {
|
|
15
|
+
'tm-mark': { k: 'tm', mark: '' },
|
|
16
|
+
'tm-type': { k: 'tm', type: 'standard', colour: false },
|
|
17
|
+
'tm-owner': { k: 'tm', rows: [] },
|
|
18
|
+
'tm-classes': { k: 'tm', answers: { downloadable: null, hosted: null, broker: null }, wording: {} },
|
|
19
|
+
'tm-basis': { k: 'tm', perClass: {} },
|
|
20
|
+
'tm-specimen': { k: 'tm', perClass: {} },
|
|
21
|
+
'tm-signatory': { k: 'tm', name: '', email: '' },
|
|
22
|
+
'tm-search': { k: 'tm', rows: [] },
|
|
23
|
+
};
|
|
24
|
+
function emptyValue(editor) {
|
|
25
|
+
switch (editor) {
|
|
26
|
+
case 'text':
|
|
27
|
+
case 'textarea': return { k: 'text', t: '' };
|
|
28
|
+
case 'steps': return { k: 'steps', rows: [] };
|
|
29
|
+
case 'proposals': return { k: 'proposals', cards: [] };
|
|
30
|
+
case 'people': return { k: 'people', rows: [] };
|
|
31
|
+
case 'where': return { k: 'where', side: null, files: [] };
|
|
32
|
+
case 'events': return { k: 'events', none: false };
|
|
33
|
+
case 'date': return { k: 'date', date: null, evIds: [] };
|
|
34
|
+
case 'list': return { k: 'list', rows: [] };
|
|
35
|
+
case 'figures': return { k: 'figures', rows: [] };
|
|
36
|
+
case 'owner': return { k: 'owner', entity: '', signed: {} };
|
|
37
|
+
default: return structuredClone(TM_EMPTY[editor] ?? { k: 'tm' });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function emptyIp(track, kind) {
|
|
41
|
+
const units = {};
|
|
42
|
+
for (const s of UNIT_SHAPES[track])
|
|
43
|
+
units[s.id] = { id: s.id, status: 'empty', rev: 0, value: emptyValue(s.editor), provenance: null, history: [] };
|
|
44
|
+
return { v: 1, track, kind: kind ?? (track === 'patent' ? 'provisional' : 'application'), title: '', blurb: '', started: false, sourceTicks: {}, units, evidence: [], events: [], filing: null, prefill: null };
|
|
45
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const PX_PER_WEEK = 120;
|
|
2
|
+
export declare const LEVELS: readonly ["objective", "key-result", "epic", "feature", "story", "enabler", "milestone"];
|
|
3
|
+
export type Level = (typeof LEVELS)[number];
|
|
4
|
+
export interface LayoutNode {
|
|
5
|
+
id: string;
|
|
6
|
+
type?: string;
|
|
7
|
+
position?: {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
};
|
|
11
|
+
data?: Record<string, any>;
|
|
12
|
+
}
|
|
13
|
+
export interface LayoutOptions {
|
|
14
|
+
now?: Date;
|
|
15
|
+
heights?: Record<string, number>;
|
|
16
|
+
}
|
|
17
|
+
export interface LayoutResult {
|
|
18
|
+
positions: Map<string, {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
}>;
|
|
22
|
+
widthOverride: Map<string, number | null>;
|
|
23
|
+
}
|
|
24
|
+
export declare function calendarStart(now: Date): Date;
|
|
25
|
+
export declare const isoDate: (d: Date) => string;
|
|
26
|
+
export declare function columnOf(date: Date, now: Date): number;
|
|
27
|
+
export declare function dateAtColumn(col: number, now: Date): Date;
|
|
28
|
+
export declare function todayColumn(now: Date): number;
|
|
29
|
+
export declare function sprintAtColumn(col: number): {
|
|
30
|
+
pi: string;
|
|
31
|
+
sprint: string;
|
|
32
|
+
};
|
|
33
|
+
export declare function scheduleFields(col: number, weeks: number, now: Date): Record<string, unknown>;
|
|
34
|
+
export declare const levelOf: (n: LayoutNode) => Level;
|
|
35
|
+
export declare const isItem: (n: LayoutNode) => boolean;
|
|
36
|
+
export declare const isIpNode: (n: LayoutNode) => boolean;
|
|
37
|
+
export declare function layoutTimeline(nodes: LayoutNode[], opts?: LayoutOptions): LayoutResult;
|
|
38
|
+
export declare function applyLayout<T extends LayoutNode>(nodes: T[], res: LayoutResult): T[];
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { IP_NODE_SIZE } from './ipShape.js';
|
|
2
|
+
export const PX_PER_WEEK = 120;
|
|
3
|
+
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
4
|
+
const ITEM_TOP_HANDLE_X = 8;
|
|
5
|
+
const LANE_GAP_X = 24;
|
|
6
|
+
const ROW_GAP = 32;
|
|
7
|
+
const BLOCK_GAP = 120;
|
|
8
|
+
export const LEVELS = ['objective', 'key-result', 'epic', 'feature', 'story', 'enabler', 'milestone'];
|
|
9
|
+
const DEFAULT_DURATION = { epic: 8, feature: 4, story: 1, enabler: 2, milestone: 0, objective: 12, 'key-result': 12 };
|
|
10
|
+
const DEFAULT_WIDTH = { epic: 280, feature: 260, story: 240, enabler: 240, milestone: 120, objective: 280, 'key-result': 260 };
|
|
11
|
+
const MIN_PAINT = { epic: 360, feature: 360, story: 352, enabler: 352 };
|
|
12
|
+
const HEIGHT = { epic: 265, feature: 265, story: 220, enabler: 220, milestone: 54, objective: 200, 'key-result': 220 };
|
|
13
|
+
const HERO_EXTRA = 150;
|
|
14
|
+
export function calendarStart(now) {
|
|
15
|
+
const s = new Date(now.getFullYear(), now.getMonth() - 3, 1);
|
|
16
|
+
const d = s.getDay();
|
|
17
|
+
s.setDate(s.getDate() + (d === 0 ? -6 : 1 - d));
|
|
18
|
+
return s;
|
|
19
|
+
}
|
|
20
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
21
|
+
export const isoDate = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
22
|
+
function parseIso(s) {
|
|
23
|
+
if (typeof s !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(s))
|
|
24
|
+
return null;
|
|
25
|
+
const out = new Date(Number(s.slice(0, 4)), Number(s.slice(5, 7)) - 1, Number(s.slice(8, 10)));
|
|
26
|
+
return Number.isNaN(out.getTime()) ? null : out;
|
|
27
|
+
}
|
|
28
|
+
export function columnOf(date, now) {
|
|
29
|
+
return Math.round((date.getTime() - calendarStart(now).getTime()) / WEEK_MS);
|
|
30
|
+
}
|
|
31
|
+
export function dateAtColumn(col, now) {
|
|
32
|
+
const d = calendarStart(now);
|
|
33
|
+
d.setDate(d.getDate() + Math.round(col * 7));
|
|
34
|
+
return d;
|
|
35
|
+
}
|
|
36
|
+
export function todayColumn(now) {
|
|
37
|
+
return Math.max(0, Math.floor((now.getTime() - calendarStart(now).getTime()) / WEEK_MS));
|
|
38
|
+
}
|
|
39
|
+
export function sprintAtColumn(col) {
|
|
40
|
+
const c = Math.max(0, col);
|
|
41
|
+
const L = ['S1', 'S2', 'S3', 'S4', 'S5', 'IP'];
|
|
42
|
+
return { pi: `PI-${Math.floor(c / 12) + 1}`, sprint: L[Math.floor((c % 12) / 2)] ?? 'S1' };
|
|
43
|
+
}
|
|
44
|
+
export function scheduleFields(col, weeks, now) {
|
|
45
|
+
const s = sprintAtColumn(col);
|
|
46
|
+
const e = sprintAtColumn(col + Math.round(weeks));
|
|
47
|
+
return {
|
|
48
|
+
durationWeeks: weeks,
|
|
49
|
+
startDate: isoDate(dateAtColumn(col, now)),
|
|
50
|
+
endDate: isoDate(dateAtColumn(col + weeks, now)),
|
|
51
|
+
pi: s.pi, sprint: s.sprint, endPi: e.pi, endSprint: e.sprint,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export const levelOf = (n) => LEVELS.includes(n.data?.hierarchyLevel) ? n.data.hierarchyLevel : 'story';
|
|
55
|
+
export const isItem = (n) => n.type === 'portfolio-item';
|
|
56
|
+
export const isIpNode = (n) => n.type === 'rad-module' && n.data?.subtype === 'ip';
|
|
57
|
+
export function layoutTimeline(nodes, opts = {}) {
|
|
58
|
+
const now = opts.now ?? new Date();
|
|
59
|
+
const heights = opts.heights ?? {};
|
|
60
|
+
const items = nodes.filter(isItem);
|
|
61
|
+
const byId = new Map(items.map(n => [n.id, n]));
|
|
62
|
+
const positions = new Map();
|
|
63
|
+
const widthOverride = new Map();
|
|
64
|
+
const geo = new Map();
|
|
65
|
+
for (const n of items) {
|
|
66
|
+
const level = levelOf(n);
|
|
67
|
+
const start = parseIso(n.data?.startDate);
|
|
68
|
+
const col = start ? columnOf(start, now) : Math.round((n.position?.x ?? 0) / PX_PER_WEEK);
|
|
69
|
+
const weeks = typeof n.data?.durationWeeks === 'number' ? n.data.durationWeeks : DEFAULT_DURATION[level];
|
|
70
|
+
const natural = weeks > 0 ? weeks * PX_PER_WEEK : DEFAULT_WIDTH[level];
|
|
71
|
+
const min = MIN_PAINT[level] ?? 0;
|
|
72
|
+
widthOverride.set(n.id, natural < min ? min : null);
|
|
73
|
+
const hero = Array.isArray(n.data?.pastedImages) && n.data.pastedImages.length > 0;
|
|
74
|
+
geo.set(n.id, { x: col * PX_PER_WEEK, width: Math.max(natural, min), height: heights[n.id] ?? HEIGHT[level] + (hero && (level === 'epic' || level === 'feature') ? HERO_EXTRA : 0) });
|
|
75
|
+
}
|
|
76
|
+
const kids = new Map();
|
|
77
|
+
for (const n of items) {
|
|
78
|
+
const p = n.data?.parentItemId;
|
|
79
|
+
if (p && geo.has(p))
|
|
80
|
+
kids.set(p, [...(kids.get(p) ?? []), n.id]);
|
|
81
|
+
}
|
|
82
|
+
const right = new Map();
|
|
83
|
+
const paintedRight = (id, depth = 0) => {
|
|
84
|
+
if (right.has(id))
|
|
85
|
+
return right.get(id);
|
|
86
|
+
const g = geo.get(id);
|
|
87
|
+
let r = g.x + g.width;
|
|
88
|
+
if (depth < 12)
|
|
89
|
+
for (const k of kids.get(id) ?? [])
|
|
90
|
+
r = Math.max(r, paintedRight(k, depth + 1) + 20);
|
|
91
|
+
right.set(id, r);
|
|
92
|
+
return r;
|
|
93
|
+
};
|
|
94
|
+
for (const [id, g] of geo)
|
|
95
|
+
g.width = paintedRight(id) - g.x;
|
|
96
|
+
const epicOf = (n) => {
|
|
97
|
+
let cur = n;
|
|
98
|
+
for (let i = 0; cur && i < 12; i++) {
|
|
99
|
+
if (levelOf(cur) === 'epic')
|
|
100
|
+
return cur.id;
|
|
101
|
+
cur = cur.data?.parentItemId ? byId.get(cur.data.parentItemId) : undefined;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
};
|
|
105
|
+
const placeBand = (slots, top) => {
|
|
106
|
+
const lanes = [];
|
|
107
|
+
const laneOf = new Map();
|
|
108
|
+
for (const s of slots) {
|
|
109
|
+
const free = (i) => !lanes[i] || lanes[i].spans.every(([a, b]) => s.x + s.width + LANE_GAP_X <= a || b + LANE_GAP_X <= s.x);
|
|
110
|
+
const parentLane = s.from !== undefined ? laneOf.get(s.from) : undefined;
|
|
111
|
+
let l = 0;
|
|
112
|
+
if (parentLane !== undefined && free(parentLane))
|
|
113
|
+
l = parentLane;
|
|
114
|
+
else
|
|
115
|
+
while (!free(l))
|
|
116
|
+
l++;
|
|
117
|
+
if (!lanes[l])
|
|
118
|
+
lanes[l] = { spans: [], height: 0 };
|
|
119
|
+
lanes[l].spans.push([s.x, s.x + s.width]);
|
|
120
|
+
lanes[l].height = Math.max(lanes[l].height, s.height);
|
|
121
|
+
laneOf.set(s.id, l);
|
|
122
|
+
}
|
|
123
|
+
const laneY = [];
|
|
124
|
+
let y = top;
|
|
125
|
+
lanes.forEach((lane, i) => { laneY[i] = y; y += lane.height + 24; });
|
|
126
|
+
for (const s of slots)
|
|
127
|
+
positions.set(s.id, { x: s.x, y: laneY[laneOf.get(s.id)] });
|
|
128
|
+
return lanes.length ? y - 24 + ROW_GAP : top;
|
|
129
|
+
};
|
|
130
|
+
const slot = (id, from) => ({ id, ...geo.get(id), ...(from ? { from } : {}) });
|
|
131
|
+
const byX = (a, b) => geo.get(a.id).x - geo.get(b.id).x || a.id.localeCompare(b.id);
|
|
132
|
+
let y = 0;
|
|
133
|
+
for (const o of items.filter(n => levelOf(n) === 'objective').sort(byX))
|
|
134
|
+
y = placeBand([slot(o.id)], y);
|
|
135
|
+
y = placeBand(items.filter(n => levelOf(n) === 'key-result').sort(byX).map(n => slot(n.id)), y);
|
|
136
|
+
const ipNodes = nodes.filter(isIpNode);
|
|
137
|
+
const ipFirstSource = new Map();
|
|
138
|
+
const ipUnplaced = [];
|
|
139
|
+
for (const n of ipNodes) {
|
|
140
|
+
const src = (n.data?.attachedItemIds ?? []).find((id) => geo.has(id));
|
|
141
|
+
if (src)
|
|
142
|
+
ipFirstSource.set(n.id, src);
|
|
143
|
+
else
|
|
144
|
+
ipUnplaced.push(n.id);
|
|
145
|
+
}
|
|
146
|
+
const epics = items.filter(n => levelOf(n) === 'epic').sort(byX);
|
|
147
|
+
const rest = items.filter(n => !['objective', 'key-result', 'epic'].includes(levelOf(n)));
|
|
148
|
+
for (const epic of [...epics.map(e => e.id), null]) {
|
|
149
|
+
const inBlock = rest.filter(n => epicOf(n) === epic);
|
|
150
|
+
const blockIds = new Set([...(epic ? [epic] : []), ...inBlock.map(n => n.id)]);
|
|
151
|
+
const ips = ipNodes.filter(n => blockIds.has(ipFirstSource.get(n.id) ?? ''));
|
|
152
|
+
if (!epic && !inBlock.length && !ips.length)
|
|
153
|
+
continue;
|
|
154
|
+
if (y > 0)
|
|
155
|
+
y += BLOCK_GAP - ROW_GAP;
|
|
156
|
+
if (epic)
|
|
157
|
+
y = placeBand([slot(epic)], y);
|
|
158
|
+
if (ips.length) {
|
|
159
|
+
const src = (id) => geo.get(ipFirstSource.get(id));
|
|
160
|
+
y = placeBand(ips.map(n => ({ id: n.id, x: src(n.id).x + ITEM_TOP_HANDLE_X - IP_NODE_SIZE.width / 2, width: IP_NODE_SIZE.width, height: IP_NODE_SIZE.height })).sort((a, b) => a.x - b.x), y) - ROW_GAP + 16;
|
|
161
|
+
}
|
|
162
|
+
const features = inBlock.filter(n => levelOf(n) === 'feature').sort(byX);
|
|
163
|
+
const leaves = inBlock.filter(n => levelOf(n) === 'story' || levelOf(n) === 'enabler').sort(byX);
|
|
164
|
+
const underFeature = (f) => leaves.filter(n => n.data?.parentItemId === f);
|
|
165
|
+
const pool = features.map(f => slot(f.id));
|
|
166
|
+
for (const f of features)
|
|
167
|
+
pool.push(...underFeature(f.id).map(n => slot(n.id, f.id)));
|
|
168
|
+
const featureIds = new Set(features.map(f => f.id));
|
|
169
|
+
pool.push(...leaves.filter(n => !featureIds.has(n.data?.parentItemId)).map(n => slot(n.id)));
|
|
170
|
+
pool.push(...inBlock.filter(n => levelOf(n) === 'milestone').sort(byX).map(n => slot(n.id)));
|
|
171
|
+
y = placeBand(pool, y);
|
|
172
|
+
}
|
|
173
|
+
ipUnplaced.forEach((id, k) => positions.set(id, { x: k * (IP_NODE_SIZE.width + LANE_GAP_X), y: y + BLOCK_GAP }));
|
|
174
|
+
return { positions, widthOverride };
|
|
175
|
+
}
|
|
176
|
+
export function applyLayout(nodes, res) {
|
|
177
|
+
return nodes.map(n => {
|
|
178
|
+
const p = res.positions.get(n.id);
|
|
179
|
+
if (!p)
|
|
180
|
+
return n;
|
|
181
|
+
const out = { ...n, position: { x: Math.round(p.x), y: Math.round(p.y) } };
|
|
182
|
+
if (res.widthOverride.has(n.id)) {
|
|
183
|
+
const data = { ...(n.data ?? {}) };
|
|
184
|
+
const w = res.widthOverride.get(n.id);
|
|
185
|
+
if (w)
|
|
186
|
+
data.manualWidthOverride = w;
|
|
187
|
+
else
|
|
188
|
+
delete data.manualWidthOverride;
|
|
189
|
+
out.data = data;
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
});
|
|
193
|
+
}
|
package/dist/core/server.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { ApiContext } from './config.js';
|
|
3
3
|
export declare const SERVER_NAME = "nonbot-mcp";
|
|
4
|
-
export declare const SERVER_VERSION = "0.
|
|
4
|
+
export declare const SERVER_VERSION = "0.2.2";
|
|
5
5
|
export declare const INSTRUCTIONS: string;
|
|
6
6
|
export declare function registerAll(server: McpServer, ctx: ApiContext): void;
|
|
7
7
|
export declare function createNonbotMcpServer(ctx: ApiContext): McpServer;
|
package/dist/core/server.js
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { registerPortfolioTools } from './toolsets/portfolio.js';
|
|
3
3
|
import { registerIpTools } from './toolsets/ip.js';
|
|
4
|
+
import { registerBuildTools } from './toolsets/build.js';
|
|
4
5
|
import { registerWhygraphTools } from './toolsets/whygraph.js';
|
|
5
6
|
import { registerRunTools } from './toolsets/run.js';
|
|
6
7
|
import { registerMonitorTools } from './toolsets/monitor.js';
|
|
7
8
|
import { registerResources } from './resources.js';
|
|
8
9
|
import { registerPrompts } from './prompts.js';
|
|
9
10
|
export const SERVER_NAME = 'nonbot-mcp';
|
|
10
|
-
export const SERVER_VERSION = '0.
|
|
11
|
+
export const SERVER_VERSION = '0.2.2';
|
|
11
12
|
export const INSTRUCTIONS = [
|
|
12
13
|
'non.bot MCP — work against the caller’s real product-management data, not a copy.',
|
|
13
14
|
'Two domains: PORTFOLIOS (the SAFe portfolio canvas — epics → features → stories) and',
|
|
14
15
|
'WHYGRAPH (the PM decision graph — the rules and decisions the team has committed to).',
|
|
15
16
|
'Read before you write: call the *_list / *_get / *_search tools first.',
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
17
|
+
'BUILDING: portfolio_create makes an empty portfolio; portfolio_add_items adds items of any level',
|
|
18
|
+
'(parents / OKR links / dependencies by id or by a key from the same call) and lays the Timeline out;',
|
|
19
|
+
'portfolio_patch_item changes one item; portfolio_layout re-lays everything; portfolio_set_meta sets',
|
|
20
|
+
'context text and palette; portfolio_asset_upload stores an image; ip_create adds an IP node over its sources.',
|
|
21
|
+
'Whole-graph rewrites (restructures, deletes): portfolio_get for the graph + version, then portfolio_update',
|
|
22
|
+
'(pass expectedVersion for safe concurrent editing).',
|
|
19
23
|
'Before a non-trivial decision, call whygraph_context to surface the LAWs and prior',
|
|
20
24
|
'decisions that apply. WhyGraph is read/reasoning-only over this transport.',
|
|
21
25
|
'MONITORING: run_feed / run_daemon_status / run_machines / run_prompts_list answer',
|
|
@@ -27,6 +31,7 @@ export const INSTRUCTIONS = [
|
|
|
27
31
|
].join(' ');
|
|
28
32
|
export function registerAll(server, ctx) {
|
|
29
33
|
registerPortfolioTools(server, ctx);
|
|
34
|
+
registerBuildTools(server, ctx);
|
|
30
35
|
registerIpTools(server, ctx);
|
|
31
36
|
registerWhygraphTools(server, ctx);
|
|
32
37
|
registerRunTools(server, ctx);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import type { ApiContext } from '../config.js';
|
|
4
|
+
import { type LayoutNode } from '../layout.js';
|
|
5
|
+
type Node = LayoutNode & {
|
|
6
|
+
id: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
data?: Record<string, any>;
|
|
9
|
+
};
|
|
10
|
+
type Edge = {
|
|
11
|
+
id: string;
|
|
12
|
+
source: string;
|
|
13
|
+
target: string;
|
|
14
|
+
[k: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
interface Graph {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
nodes: Node[];
|
|
20
|
+
edges: Edge[];
|
|
21
|
+
viewport: unknown;
|
|
22
|
+
version?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare const toHtml: (s: string) => string;
|
|
25
|
+
export declare function newItemId(now?: number): string;
|
|
26
|
+
declare const itemSpec: z.ZodObject<{
|
|
27
|
+
key: z.ZodOptional<z.ZodString>;
|
|
28
|
+
level: z.ZodEnum<{
|
|
29
|
+
epic: "epic";
|
|
30
|
+
objective: "objective";
|
|
31
|
+
"key-result": "key-result";
|
|
32
|
+
feature: "feature";
|
|
33
|
+
story: "story";
|
|
34
|
+
enabler: "enabler";
|
|
35
|
+
milestone: "milestone";
|
|
36
|
+
}>;
|
|
37
|
+
title: z.ZodString;
|
|
38
|
+
description: z.ZodOptional<z.ZodString>;
|
|
39
|
+
bluf: z.ZodOptional<z.ZodString>;
|
|
40
|
+
acceptance_criteria: z.ZodOptional<z.ZodString>;
|
|
41
|
+
hypothesis: z.ZodOptional<z.ZodString>;
|
|
42
|
+
state: z.ZodOptional<z.ZodEnum<{
|
|
43
|
+
done: "done";
|
|
44
|
+
funnel: "funnel";
|
|
45
|
+
reviewing: "reviewing";
|
|
46
|
+
analyzing: "analyzing";
|
|
47
|
+
ready: "ready";
|
|
48
|
+
implementing: "implementing";
|
|
49
|
+
}>>;
|
|
50
|
+
parent: z.ZodOptional<z.ZodString>;
|
|
51
|
+
okr: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
52
|
+
depends_on: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
53
|
+
start_week: z.ZodOptional<z.ZodNumber>;
|
|
54
|
+
start_date: z.ZodOptional<z.ZodString>;
|
|
55
|
+
duration_weeks: z.ZodOptional<z.ZodNumber>;
|
|
56
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
57
|
+
wsjf: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
|
|
58
|
+
owner: z.ZodOptional<z.ZodString>;
|
|
59
|
+
kr_target: z.ZodOptional<z.ZodString>;
|
|
60
|
+
kr_current: z.ZodOptional<z.ZodString>;
|
|
61
|
+
okr_status: z.ZodOptional<z.ZodEnum<{
|
|
62
|
+
"on-track": "on-track";
|
|
63
|
+
"at-risk": "at-risk";
|
|
64
|
+
behind: "behind";
|
|
65
|
+
}>>;
|
|
66
|
+
images: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
67
|
+
fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
68
|
+
}, z.core.$strip>;
|
|
69
|
+
type ItemSpec = z.infer<typeof itemSpec>;
|
|
70
|
+
export declare function buildItems(g: Graph, specs: ItemSpec[], now?: Date): {
|
|
71
|
+
nodes: Node[];
|
|
72
|
+
edges: Edge[];
|
|
73
|
+
ids: Record<string, string>;
|
|
74
|
+
errors: string[];
|
|
75
|
+
patched: Map<string, Record<string, any>>;
|
|
76
|
+
};
|
|
77
|
+
export declare function registerBuildTools(server: McpServer, ctx: ApiContext): void;
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { api, expectJson } from '../http.js';
|
|
3
|
+
import { validateGraph } from '../validate.js';
|
|
4
|
+
import { LEVELS, applyLayout, layoutTimeline, scheduleFields, todayColumn, columnOf, isItem, PX_PER_WEEK } from '../layout.js';
|
|
5
|
+
import { emptyIp, IP_KINDS, IP_NODE_SIZE } from '../ipShape.js';
|
|
6
|
+
const STATES = ['funnel', 'reviewing', 'analyzing', 'ready', 'implementing', 'done'];
|
|
7
|
+
const OKR_STATUSES = ['on-track', 'at-risk', 'behind'];
|
|
8
|
+
const RESERVED = new Set(['hierarchyLevel', 'parentItemId', 'linkedEpicIds', 'id', '__proto__', 'constructor', 'prototype']);
|
|
9
|
+
const MAX_ITEMS = 200;
|
|
10
|
+
const MAX_ASSET_BYTES = 2_200_000;
|
|
11
|
+
const text = (obj) => ({ type: 'text', text: JSON.stringify(obj, null, 2) });
|
|
12
|
+
const fail = (msg) => ({ content: [{ type: 'text', text: msg }], isError: true });
|
|
13
|
+
const escHtml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
14
|
+
export const toHtml = (s) => /^\s*</.test(s) ? s : s.split(/\n{2,}/).map(p => `<p>${escHtml(p.trim()).replace(/\n/g, '<br>')}</p>`).join('');
|
|
15
|
+
export function newItemId(now = Date.now()) { return `pf-${now}-${Math.random().toString(36).slice(2, 11)}`; }
|
|
16
|
+
async function loadGraph(ctx, id) {
|
|
17
|
+
const data = await expectJson(await api(ctx, `/api/portfolio/graph?id=${encodeURIComponent(id)}`), 'load portfolio');
|
|
18
|
+
const g = (data.graph ?? data);
|
|
19
|
+
return {
|
|
20
|
+
name: String(data.name ?? g.name ?? ''),
|
|
21
|
+
description: String(data.description ?? g.description ?? ''),
|
|
22
|
+
nodes: Array.isArray(g.nodes) ? g.nodes : [],
|
|
23
|
+
edges: Array.isArray(g.edges) ? g.edges : [],
|
|
24
|
+
viewport: g.viewport ?? { x: 0, y: 0, zoom: 1 },
|
|
25
|
+
version: typeof g.version === 'number' ? g.version : undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function saveGraph(ctx, id, before, after, expectedVersion) {
|
|
29
|
+
const problems = validateGraph(after.nodes, after.edges);
|
|
30
|
+
if (problems.length)
|
|
31
|
+
return { ok: false, message: `Refusing to write — ${problems.length} problem(s):\n` + problems.map(p => ` - ${p}`).join('\n') };
|
|
32
|
+
if (ctx.backup) {
|
|
33
|
+
try {
|
|
34
|
+
await ctx.backup(id, before);
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
}
|
|
38
|
+
const res = await api(ctx, '/api/portfolio/graph', {
|
|
39
|
+
method: 'PUT',
|
|
40
|
+
body: JSON.stringify({ id, name: after.name, description: after.description, nodes: after.nodes, edges: after.edges, viewport: after.viewport, expectedVersion: expectedVersion ?? before.version }),
|
|
41
|
+
});
|
|
42
|
+
if (res.status === 409) {
|
|
43
|
+
const c = (await res.json().catch(() => ({})));
|
|
44
|
+
return { ok: false, message: `Refused (409) — the portfolio changed since it was read (now version ${c.currentVersion ?? '?'}). Nothing was written; call again.` };
|
|
45
|
+
}
|
|
46
|
+
const out = await expectJson(res, 'save portfolio');
|
|
47
|
+
return { ok: true, version: typeof out.version === 'number' ? out.version : undefined };
|
|
48
|
+
}
|
|
49
|
+
const itemSpec = z.object({
|
|
50
|
+
key: z.string().max(64).optional().describe('A local name for this item, so other items in the SAME call can name it as parent / okr / depends_on before it has an id.'),
|
|
51
|
+
level: z.enum(LEVELS),
|
|
52
|
+
title: z.string().min(1).max(200),
|
|
53
|
+
description: z.string().max(20000).optional().describe('Plain text (blank lines = paragraphs) or HTML.'),
|
|
54
|
+
bluf: z.string().max(4000).optional().describe('Bottom line up front — the sentence the card shows.'),
|
|
55
|
+
acceptance_criteria: z.string().max(8000).optional(),
|
|
56
|
+
hypothesis: z.string().max(4000).optional().describe('Hypothesis statement ("We believe … will … measured by …").'),
|
|
57
|
+
state: z.enum(STATES).optional().describe('Default funnel.'),
|
|
58
|
+
parent: z.string().optional().describe('Parent item id, or a key from this call. Epic → feature/enabler/milestone, feature → story/enabler/milestone, objective → key-result.'),
|
|
59
|
+
okr: z.array(z.string()).max(10).optional().describe('Epics only: key-result ids/keys this epic contributes to (an okr-link edge + the KR roll-up).'),
|
|
60
|
+
depends_on: z.array(z.string()).max(20).optional().describe('Finish-to-start predecessors (ids or keys).'),
|
|
61
|
+
start_week: z.number().int().min(-52).max(156).optional().describe('Start, in weeks from the Monday of the current week (negative = past). Ignored when start_date is given.'),
|
|
62
|
+
start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
63
|
+
duration_weeks: z.number().min(0).max(156).optional().describe('Default: epic 8, feature 4, story 1, enabler 2, milestone 0, objective / key-result 12.'),
|
|
64
|
+
tags: z.array(z.string().max(40)).max(20).optional(),
|
|
65
|
+
wsjf: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional().describe('[business value, time criticality, risk reduction, job size].'),
|
|
66
|
+
owner: z.string().max(80).optional(),
|
|
67
|
+
kr_target: z.string().max(200).optional(),
|
|
68
|
+
kr_current: z.string().max(200).optional(),
|
|
69
|
+
okr_status: z.enum(OKR_STATUSES).optional(),
|
|
70
|
+
images: z.array(z.string().max(2048)).max(8).optional().describe('Image URLs from portfolio_asset_upload — the first is the card hero.'),
|
|
71
|
+
fields: z.record(z.string(), z.unknown()).optional().describe('Any other PortfolioItemData fields, merged last (structural keys are refused).'),
|
|
72
|
+
});
|
|
73
|
+
const DEFAULT_WEEKS = { epic: 8, feature: 4, story: 1, enabler: 2, milestone: 0, objective: 12, 'key-result': 12 };
|
|
74
|
+
const LEGAL_PARENT = {
|
|
75
|
+
epic: [], objective: [], 'key-result': ['objective'], feature: ['epic'], story: ['feature', 'epic'], enabler: ['epic', 'feature'], milestone: ['epic', 'feature'],
|
|
76
|
+
};
|
|
77
|
+
export function buildItems(g, specs, now = new Date()) {
|
|
78
|
+
const errors = [];
|
|
79
|
+
const ids = {};
|
|
80
|
+
const t0 = now.getTime();
|
|
81
|
+
const idOf = specs.map((s, i) => { const id = newItemId(t0 + i); ids[s.key ?? `#${i}`] = id; return id; });
|
|
82
|
+
const levelById = new Map(g.nodes.filter(isItem).map(n => [n.id, String(n.data?.hierarchyLevel)]));
|
|
83
|
+
specs.forEach((s, i) => levelById.set(idOf[i], s.level));
|
|
84
|
+
const resolve = (ref) => ids[ref] ?? (levelById.has(ref) ? ref : null);
|
|
85
|
+
const base = todayColumn(now);
|
|
86
|
+
const nodes = [];
|
|
87
|
+
const edges = [];
|
|
88
|
+
const krLinks = new Map();
|
|
89
|
+
specs.forEach((s, i) => {
|
|
90
|
+
const self = idOf[i];
|
|
91
|
+
const label = s.key ?? s.title;
|
|
92
|
+
const d = { hierarchyLevel: s.level, title: s.title, description: s.description ? toHtml(s.description) : '', state: s.state ?? 'funnel', createdAt: t0, updatedAt: t0 };
|
|
93
|
+
if (s.parent) {
|
|
94
|
+
const p = resolve(s.parent);
|
|
95
|
+
if (!p)
|
|
96
|
+
errors.push(`${label}: parent "${s.parent}" is neither an id in the portfolio nor a key in this call`);
|
|
97
|
+
else if (!(LEGAL_PARENT[s.level] ?? []).includes(levelById.get(p) ?? ''))
|
|
98
|
+
errors.push(`${label}: a ${s.level} cannot sit under a ${levelById.get(p)} (allowed: ${(LEGAL_PARENT[s.level] ?? []).join(', ') || 'none — top level'})`);
|
|
99
|
+
else {
|
|
100
|
+
d.parentItemId = p;
|
|
101
|
+
edges.push({ id: `edge-${p}-${self}`, source: p, target: self, dependencyType: 'hierarchy' });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const weeks = s.duration_weeks ?? DEFAULT_WEEKS[s.level] ?? 1;
|
|
105
|
+
const col = s.start_date ? columnOf(new Date(Number(s.start_date.slice(0, 4)), Number(s.start_date.slice(5, 7)) - 1, Number(s.start_date.slice(8, 10))), now) : base + (s.start_week ?? 0);
|
|
106
|
+
Object.assign(d, scheduleFields(col, weeks, now));
|
|
107
|
+
if (s.bluf)
|
|
108
|
+
d.bluf = toHtml(s.bluf);
|
|
109
|
+
if (s.acceptance_criteria)
|
|
110
|
+
d.acceptanceCriteria = toHtml(s.acceptance_criteria);
|
|
111
|
+
if (s.hypothesis)
|
|
112
|
+
d.hypothesisStatement = s.hypothesis;
|
|
113
|
+
if (s.tags)
|
|
114
|
+
d.tags = s.tags;
|
|
115
|
+
if (s.owner)
|
|
116
|
+
d.owner = s.owner;
|
|
117
|
+
if (s.wsjf)
|
|
118
|
+
Object.assign(d, { wsjfBusinessValue: s.wsjf[0], wsjfTimeCriticality: s.wsjf[1], wsjfRiskReduction: s.wsjf[2], wsjfJobSize: s.wsjf[3] });
|
|
119
|
+
if (s.kr_target)
|
|
120
|
+
d.krTarget = s.kr_target;
|
|
121
|
+
if (s.kr_current)
|
|
122
|
+
d.krCurrent = s.kr_current;
|
|
123
|
+
if (s.okr_status)
|
|
124
|
+
d.okrStatus = s.okr_status;
|
|
125
|
+
if (s.images?.length)
|
|
126
|
+
d.pastedImages = s.images;
|
|
127
|
+
if (s.level === 'epic')
|
|
128
|
+
d.innerLoopExpanded = false;
|
|
129
|
+
for (const [k, v] of Object.entries(s.fields ?? {})) {
|
|
130
|
+
if (RESERVED.has(k))
|
|
131
|
+
errors.push(`${label}: fields.${k} is structural — use the tool's own parameter`);
|
|
132
|
+
else
|
|
133
|
+
d[k] = v;
|
|
134
|
+
}
|
|
135
|
+
for (const kr of s.okr ?? []) {
|
|
136
|
+
const k = resolve(kr);
|
|
137
|
+
if (s.level !== 'epic')
|
|
138
|
+
errors.push(`${label}: okr links are for epics`);
|
|
139
|
+
else if (!k || levelById.get(k) !== 'key-result')
|
|
140
|
+
errors.push(`${label}: okr "${kr}" is not a key result`);
|
|
141
|
+
else {
|
|
142
|
+
edges.push({ id: `edge-${k}-${self}`, source: k, target: self, dependencyType: 'okr-link' });
|
|
143
|
+
krLinks.set(k, [...(krLinks.get(k) ?? []), self]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const dep of s.depends_on ?? []) {
|
|
147
|
+
const p = resolve(dep);
|
|
148
|
+
if (!p)
|
|
149
|
+
errors.push(`${label}: depends_on "${dep}" not found`);
|
|
150
|
+
else
|
|
151
|
+
edges.push({ id: `edge-${p}-${self}`, source: p, target: self, dependencyType: 'finish-to-start', label: 'FS' });
|
|
152
|
+
}
|
|
153
|
+
nodes.push({ id: self, type: 'portfolio-item', position: { x: col * PX_PER_WEEK, y: 0 }, data: d });
|
|
154
|
+
});
|
|
155
|
+
const patched = new Map();
|
|
156
|
+
for (const [kr, epics] of krLinks) {
|
|
157
|
+
const fresh = nodes.find(n => n.id === kr);
|
|
158
|
+
const old = g.nodes.find(n => n.id === kr);
|
|
159
|
+
const data = { ...((fresh ?? old)?.data ?? {}) };
|
|
160
|
+
data.linkedEpicIds = [...new Set([...(data.linkedEpicIds ?? []), ...epics])];
|
|
161
|
+
if (fresh)
|
|
162
|
+
fresh.data = data;
|
|
163
|
+
else if (old)
|
|
164
|
+
patched.set(kr, data);
|
|
165
|
+
}
|
|
166
|
+
return { nodes, edges, ids, errors, patched };
|
|
167
|
+
}
|
|
168
|
+
export function registerBuildTools(server, ctx) {
|
|
169
|
+
if (ctx.readonly)
|
|
170
|
+
return;
|
|
171
|
+
server.registerTool('portfolio_create', {
|
|
172
|
+
title: 'Create an empty portfolio',
|
|
173
|
+
description: 'Create a new, empty portfolio owned by the caller and return its id + version. Fill it with portfolio_add_items (any level) and ip_create; set its context text and palette with portfolio_set_meta.',
|
|
174
|
+
inputSchema: {
|
|
175
|
+
name: z.string().min(1).max(200),
|
|
176
|
+
description: z.string().max(2000).optional(),
|
|
177
|
+
status: z.enum(['active', 'draft']).optional().describe('Default active (the CREATE path alone leaves a new row as draft).'),
|
|
178
|
+
},
|
|
179
|
+
outputSchema: { id: z.string(), version: z.number().optional(), url: z.string(), status: z.string() },
|
|
180
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
181
|
+
}, async (a) => {
|
|
182
|
+
const out = await expectJson(await api(ctx, '/api/portfolio/graph', {
|
|
183
|
+
method: 'PUT', body: JSON.stringify({ name: a.name, description: a.description ?? '', nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } }),
|
|
184
|
+
}), 'portfolio_create');
|
|
185
|
+
const id = String(out.id ?? '');
|
|
186
|
+
if (!id)
|
|
187
|
+
return fail('portfolio_create: the server did not return an id.');
|
|
188
|
+
const status = a.status ?? 'active';
|
|
189
|
+
if (status !== 'draft')
|
|
190
|
+
await expectJson(await api(ctx, '/api/portfolio/graph', { method: 'PUT', body: JSON.stringify({ id, metaOnly: true, status }) }), 'portfolio_create (status)');
|
|
191
|
+
const structured = { id, ...(typeof out.version === 'number' ? { version: out.version } : {}), url: `${ctx.apiBase}/portfolio/${id}`, status };
|
|
192
|
+
return { content: [text(structured)], structuredContent: structured };
|
|
193
|
+
});
|
|
194
|
+
server.registerTool('portfolio_add_items', {
|
|
195
|
+
title: 'Add items to a portfolio',
|
|
196
|
+
description: 'Add up to 200 items of any level (objective, key-result, epic, feature, story, enabler, milestone) in one versioned write. Parents, OKR links and dependencies may name existing ids OR `key`s of other items in the same call, so a whole tree goes in at once. The tool makes the ids, the hierarchy / okr-link / finish-to-start edges, the dates and sprint labels from start_week + duration_weeks, and (unless layout is "none") re-lays the whole portfolio on the Timeline so nothing overlaps. Returns key → id. Rich text fields accept plain text or HTML.',
|
|
197
|
+
inputSchema: {
|
|
198
|
+
portfolio_id: z.string(),
|
|
199
|
+
items: z.array(itemSpec).min(1).max(MAX_ITEMS),
|
|
200
|
+
layout: z.enum(['timeline', 'none']).optional().describe('Default timeline: re-lay every item after adding. "none": new items get their date x and y 0.'),
|
|
201
|
+
expected_version: z.number().int().optional().describe('Optional OCC guard; default = the version read at call time.'),
|
|
202
|
+
},
|
|
203
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
204
|
+
}, async (a) => {
|
|
205
|
+
const g = await loadGraph(ctx, a.portfolio_id);
|
|
206
|
+
const keys = a.items.map(s => s.key).filter(Boolean);
|
|
207
|
+
if (new Set(keys).size !== keys.length)
|
|
208
|
+
return fail('Two items share a key — keys must be unique within one call.');
|
|
209
|
+
const built = buildItems(g, a.items);
|
|
210
|
+
if (built.errors.length)
|
|
211
|
+
return fail(`Nothing was written — ${built.errors.length} problem(s):\n` + built.errors.map(e => ` - ${e}`).join('\n'));
|
|
212
|
+
let nodes = [...g.nodes.map(n => built.patched.has(n.id) ? { ...n, data: built.patched.get(n.id) } : n), ...built.nodes];
|
|
213
|
+
if ((a.layout ?? 'timeline') === 'timeline')
|
|
214
|
+
nodes = applyLayout(nodes, layoutTimeline(nodes));
|
|
215
|
+
const after = { ...g, nodes, edges: [...g.edges, ...built.edges] };
|
|
216
|
+
const r = await saveGraph(ctx, a.portfolio_id, g, after, a.expected_version);
|
|
217
|
+
if (!r.ok)
|
|
218
|
+
return fail(r.message);
|
|
219
|
+
const structured = { ok: true, added: built.nodes.length, edges: built.edges.length, ids: built.ids, ...(r.version !== undefined ? { version: r.version } : {}) };
|
|
220
|
+
return { content: [text(structured)], structuredContent: structured };
|
|
221
|
+
});
|
|
222
|
+
server.registerTool('portfolio_patch_item', {
|
|
223
|
+
title: 'Patch one item',
|
|
224
|
+
description: 'Change fields of ONE portfolio item without rewriting the graph (PATCH /api/portfolio/graph/node): `set` writes allowlisted PortfolioItemData fields (title, description, bluf, acceptanceCriteria, hypothesisStatement, state, tags, wsjf*, krTarget, pastedImages, durationWeeks, startDate, …), `unset` deletes them. Rich text is stored as given (pass HTML, or plain text which is wrapped in <p>). A stale version answers 409 — nothing is written.',
|
|
225
|
+
inputSchema: {
|
|
226
|
+
portfolio_id: z.string(),
|
|
227
|
+
item_id: z.string(),
|
|
228
|
+
set: z.record(z.string(), z.unknown()).optional(),
|
|
229
|
+
unset: z.array(z.string()).max(40).optional(),
|
|
230
|
+
expected_version: z.number().int().optional(),
|
|
231
|
+
},
|
|
232
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
233
|
+
}, async (a) => {
|
|
234
|
+
if (!a.set && !a.unset)
|
|
235
|
+
return fail('Provide set and/or unset.');
|
|
236
|
+
let version = a.expected_version;
|
|
237
|
+
if (version === undefined)
|
|
238
|
+
version = (await loadGraph(ctx, a.portfolio_id)).version;
|
|
239
|
+
const set = {};
|
|
240
|
+
for (const [k, v] of Object.entries(a.set ?? {}))
|
|
241
|
+
set[k] = typeof v === 'string' && ['description', 'bluf', 'acceptanceCriteria'].includes(k) ? toHtml(v) : v;
|
|
242
|
+
const res = await api(ctx, '/api/portfolio/graph/node', { method: 'PATCH', body: JSON.stringify({ graphId: a.portfolio_id, nodeId: a.item_id, expectedVersion: version, ...(a.set ? { set } : {}), ...(a.unset ? { unset: a.unset } : {}) }) });
|
|
243
|
+
if (res.status === 409)
|
|
244
|
+
return fail('Refused (409) — the portfolio changed since it was read. Call again.');
|
|
245
|
+
const out = await expectJson(res, 'portfolio_patch_item');
|
|
246
|
+
const structured = { ok: true, ...(typeof out.version === 'number' ? { version: out.version } : {}) };
|
|
247
|
+
return { content: [text(structured)], structuredContent: structured };
|
|
248
|
+
});
|
|
249
|
+
server.registerTool('portfolio_layout', {
|
|
250
|
+
title: 'Lay out a portfolio on the Timeline',
|
|
251
|
+
description: 'Re-position every item so the Timeline reads cleanly: x from each item\'s start date (the canvas reads dates off x), y top-down — objectives, key results, then one block per epic (epic, features, stories + enablers, milestones), each row lane-packed so overlapping items stack, IP nodes directly above their first source. Short items get a minimum painted width so titles fit. Content is untouched. `heights` overrides the estimated card height per id (px) when a card measures taller on screen.',
|
|
252
|
+
inputSchema: {
|
|
253
|
+
portfolio_id: z.string(),
|
|
254
|
+
heights: z.record(z.string(), z.number().min(20).max(2000)).optional(),
|
|
255
|
+
expected_version: z.number().int().optional(),
|
|
256
|
+
},
|
|
257
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
258
|
+
}, async (a) => {
|
|
259
|
+
const g = await loadGraph(ctx, a.portfolio_id);
|
|
260
|
+
const res = layoutTimeline(g.nodes, { heights: a.heights });
|
|
261
|
+
const r = await saveGraph(ctx, a.portfolio_id, g, { ...g, nodes: applyLayout(g.nodes, res) }, a.expected_version);
|
|
262
|
+
if (!r.ok)
|
|
263
|
+
return fail(r.message);
|
|
264
|
+
const structured = { ok: true, positioned: res.positions.size, ...(r.version !== undefined ? { version: r.version } : {}) };
|
|
265
|
+
return { content: [text(structured)], structuredContent: structured };
|
|
266
|
+
});
|
|
267
|
+
server.registerTool('portfolio_set_meta', {
|
|
268
|
+
title: 'Set portfolio settings',
|
|
269
|
+
description: 'Graph-level settings that travel with the portfolio (the `__graph_meta__` record): `context_text` (the portfolio\'s context notes — what a newcomer reads first), `palette` ({ id, name, swatches: [{hex, label?}] ≤ 10, roles?: {epic, feature, story, …: hex | "p:N"} }), `canvas_style`, `sprint_settings`. Only the keys you pass change; everything else in the record is kept.',
|
|
270
|
+
inputSchema: {
|
|
271
|
+
portfolio_id: z.string(),
|
|
272
|
+
context_text: z.string().max(20000).optional(),
|
|
273
|
+
palette: z.object({ id: z.string().min(1).max(64), name: z.string().min(1).max(64), swatches: z.array(z.object({ hex: z.string().regex(/^#[0-9a-fA-F]{6}$/), label: z.string().max(32).optional() })).max(10), roles: z.record(z.string(), z.string()).optional() }).optional(),
|
|
274
|
+
canvas_style: z.record(z.string(), z.unknown()).optional(),
|
|
275
|
+
sprint_settings: z.record(z.string(), z.unknown()).optional(),
|
|
276
|
+
expected_version: z.number().int().optional(),
|
|
277
|
+
},
|
|
278
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
279
|
+
}, async (a) => {
|
|
280
|
+
const g = await loadGraph(ctx, a.portfolio_id);
|
|
281
|
+
const cur = g.nodes.find(n => n.id === '__graph_meta__' || n.type === 'graph-meta');
|
|
282
|
+
const data = { ...(cur?.data ?? {}) };
|
|
283
|
+
if (a.context_text !== undefined)
|
|
284
|
+
data.contextText = a.context_text;
|
|
285
|
+
if (a.palette)
|
|
286
|
+
data.palette = { ...a.palette, swatches: a.palette.swatches.map(s => ({ ...s, hex: s.hex.toUpperCase() })), roles: a.palette.roles ?? {} };
|
|
287
|
+
if (a.canvas_style)
|
|
288
|
+
data.canvasStyle = { ...(data.canvasStyle ?? {}), ...a.canvas_style };
|
|
289
|
+
if (a.sprint_settings)
|
|
290
|
+
data.sprintSettings = { ...(data.sprintSettings ?? {}), ...a.sprint_settings };
|
|
291
|
+
const meta = { id: '__graph_meta__', type: 'graph-meta', position: { x: 0, y: 0 }, data };
|
|
292
|
+
const nodes = cur ? g.nodes.map(n => n === cur ? meta : n) : [...g.nodes, meta];
|
|
293
|
+
const r = await saveGraph(ctx, a.portfolio_id, g, { ...g, nodes }, a.expected_version);
|
|
294
|
+
if (!r.ok)
|
|
295
|
+
return fail(r.message);
|
|
296
|
+
const structured = { ok: true, keys: Object.keys(data), ...(r.version !== undefined ? { version: r.version } : {}) };
|
|
297
|
+
return { content: [text(structured)], structuredContent: structured };
|
|
298
|
+
});
|
|
299
|
+
server.registerTool('portfolio_asset_upload', {
|
|
300
|
+
title: 'Upload an image',
|
|
301
|
+
description: 'Store one image (PNG / JPEG / WebP / GIF, ≤ 2.2 MB — downscale first; the app keeps images at 1024 px JPEG) in your private asset store and get back its URL, for an item\'s `images` (card hero) or IP evidence (`ip_evidence_add` kind image with {asset: key}). Pass `data_url`, or `file_path` when this server runs locally (stdio).',
|
|
302
|
+
inputSchema: {
|
|
303
|
+
data_url: z.string().regex(/^data:image\/(png|jpeg|webp|gif);base64,/).optional(),
|
|
304
|
+
file_path: z.string().max(1024).optional(),
|
|
305
|
+
},
|
|
306
|
+
outputSchema: { url: z.string(), key: z.string().optional(), bytes: z.number() },
|
|
307
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
308
|
+
}, async (a) => {
|
|
309
|
+
let dataUrl = a.data_url;
|
|
310
|
+
if (!dataUrl && a.file_path) {
|
|
311
|
+
if (!ctx.readLocalImage)
|
|
312
|
+
return fail('file_path is only available when the MCP server runs locally — pass data_url instead.');
|
|
313
|
+
dataUrl = await ctx.readLocalImage(a.file_path);
|
|
314
|
+
}
|
|
315
|
+
if (!dataUrl)
|
|
316
|
+
return fail('Provide data_url or file_path.');
|
|
317
|
+
const bytes = Math.floor((dataUrl.length - dataUrl.indexOf(',') - 1) * 3 / 4);
|
|
318
|
+
if (bytes > MAX_ASSET_BYTES)
|
|
319
|
+
return fail(`The image is ${(bytes / 1e6).toFixed(1)} MB; the limit is 2.2 MB. Downscale it (1024 px JPEG is what the app stores) and retry.`);
|
|
320
|
+
const out = await expectJson(await api(ctx, '/api/portfolio/storyboard-assets', { method: 'POST', body: JSON.stringify({ dataUrl }) }), 'portfolio_asset_upload');
|
|
321
|
+
const url = out.url ? String(out.url) : out.key ? `/api/portfolio/asset?key=${encodeURIComponent(String(out.key))}` : '';
|
|
322
|
+
if (!url)
|
|
323
|
+
return fail('portfolio_asset_upload: the server returned no url.');
|
|
324
|
+
const structured = { url, ...(out.key ? { key: String(out.key) } : {}), bytes };
|
|
325
|
+
return { content: [text(structured)], structuredContent: structured };
|
|
326
|
+
});
|
|
327
|
+
server.registerTool('ip_create', {
|
|
328
|
+
title: 'Create an IP node',
|
|
329
|
+
description: 'Add an IP node — a patent or trademark disclosure — to a portfolio, wired to its source items (the cards the disclosure is prepared FROM: their title, description, BLUF, acceptance criteria and hypothesis). Placed above its first source like the canvas does, with an empty disclosure bag; then fill it in the app (Continue → prefills from the sources on your key) or with ip_update. Requires the IP node feature to be on for the caller.',
|
|
330
|
+
inputSchema: {
|
|
331
|
+
portfolio_id: z.string(),
|
|
332
|
+
track: z.enum(['patent', 'trademark']),
|
|
333
|
+
kind: z.enum(['provisional', 'nonprovisional', 'design', 'application']).optional().describe('patent: provisional (default) / nonprovisional / design; trademark: application.'),
|
|
334
|
+
title: z.string().max(200).optional().describe('The filing\'s working title shown on the card.'),
|
|
335
|
+
sources: z.array(z.string()).min(1).max(20).describe('Item ids the disclosure reads from (first = the one it sits above).'),
|
|
336
|
+
layout: z.enum(['timeline', 'none']).optional().describe('Default timeline: re-lay the portfolio so the node gets its own row above its source. "none": place it 40 px above the source and move nothing else.'),
|
|
337
|
+
expected_version: z.number().int().optional(),
|
|
338
|
+
},
|
|
339
|
+
outputSchema: { nodeId: z.string(), version: z.number().optional() },
|
|
340
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
341
|
+
}, async (a) => {
|
|
342
|
+
const track = a.track;
|
|
343
|
+
if (a.kind && !IP_KINDS[track].includes(a.kind))
|
|
344
|
+
return fail(`kind ${a.kind} is not a ${track} kind (${IP_KINDS[track].join(', ')}).`);
|
|
345
|
+
const g = await loadGraph(ctx, a.portfolio_id);
|
|
346
|
+
const items = new Map(g.nodes.filter(isItem).map(n => [n.id, n]));
|
|
347
|
+
const missing = a.sources.filter(s => !items.has(s));
|
|
348
|
+
if (missing.length)
|
|
349
|
+
return fail(`Not items in this portfolio: ${missing.join(', ')}`);
|
|
350
|
+
const src = items.get(a.sources[0]);
|
|
351
|
+
const nodeId = `radmod-ip-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
352
|
+
const bag = { ...emptyIp(track, a.kind), title: a.title ?? '' };
|
|
353
|
+
const node = {
|
|
354
|
+
id: nodeId, type: 'rad-module',
|
|
355
|
+
position: { x: (src.position?.x ?? 0) + 8 - IP_NODE_SIZE.width / 2, y: (src.position?.y ?? 0) - IP_NODE_SIZE.height - 40 },
|
|
356
|
+
data: { subtype: 'ip', moduleData: bag, width: IP_NODE_SIZE.width, height: IP_NODE_SIZE.height, attachedItemIds: [...new Set(a.sources)] },
|
|
357
|
+
};
|
|
358
|
+
let nodes = [...g.nodes, node];
|
|
359
|
+
if ((a.layout ?? 'timeline') === 'timeline')
|
|
360
|
+
nodes = applyLayout(nodes, layoutTimeline(nodes));
|
|
361
|
+
const r = await saveGraph(ctx, a.portfolio_id, g, { ...g, nodes }, a.expected_version);
|
|
362
|
+
if (!r.ok)
|
|
363
|
+
return fail(r.message);
|
|
364
|
+
const structured = { nodeId, ...(r.version !== undefined ? { version: r.version } : {}) };
|
|
365
|
+
return { content: [text(structured)], structuredContent: structured };
|
|
366
|
+
});
|
|
367
|
+
}
|
package/dist/core/toolsets/ip.js
CHANGED
|
@@ -75,7 +75,7 @@ export function registerIpTools(server, ctx) {
|
|
|
75
75
|
server.registerTool('ip_update', {
|
|
76
76
|
title: 'Edit an IP node (op list)',
|
|
77
77
|
description: 'Apply an op list to one IP node in ONE versioned write: unit.set {unitId, value, expectedRev} · unit.confirm · unit.unconfirm · evidence.update {evId, patch} · evidence.remove {evId} (a tombstone) · evidence.witness {evId, witness:{for,name,line}|null} · evidence.add (max 5 per request; identical adds collapse into one row) · event.upsert · event.remove · filing.set · meta.set. A stale expected_version is HTTP 409 version_conflict with currentVersion — re-read with ip_get and retry.',
|
|
78
|
-
inputSchema: { ...ids, expected_version: z.number().int().describe('The graph version from ip_get.'), ops: z.array(z.record(z.unknown())).min(1).max(50).refine(o => o.filter(x => x.op === 'evidence.add').length <= 5, 'At most 5 evidence.add ops per request.').describe('The op list.') },
|
|
78
|
+
inputSchema: { ...ids, expected_version: z.number().int().describe('The graph version from ip_get.'), ops: z.array(z.record(z.string(), z.unknown())).min(1).max(50).refine(o => o.filter(x => x.op === 'evidence.add').length <= 5, 'At most 5 evidence.add ops per request.').describe('The op list.') },
|
|
79
79
|
annotations: WRITE,
|
|
80
80
|
}, async (a) => textResult(await patchNode(ctx, { portfolioId: a.portfolio_id, nodeId: a.node_id, expectedVersion: a.expected_version, ops: a.ops })));
|
|
81
81
|
server.registerTool('ip_evidence_add', {
|
|
@@ -85,7 +85,7 @@ export function registerIpTools(server, ctx) {
|
|
|
85
85
|
...ids,
|
|
86
86
|
expected_version: z.number().int().describe('The graph version from ip_get.'),
|
|
87
87
|
kind: z.enum(KINDS),
|
|
88
|
-
from: z.record(z.unknown()).describe('Exactly one source (see description).'),
|
|
88
|
+
from: z.record(z.string(), z.unknown()).describe('Exactly one source (see description).'),
|
|
89
89
|
title: z.string().min(1).max(300),
|
|
90
90
|
describes: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe('The date the evidence describes (YYYY-MM-DD).'),
|
|
91
91
|
supports: z.array(z.enum(SUPPORTS)).max(5).optional(),
|
|
@@ -222,7 +222,7 @@ export function registerPortfolioTools(server, ctx) {
|
|
|
222
222
|
}
|
|
223
223
|
server.registerTool('portfolio_update', {
|
|
224
224
|
title: 'Replace a portfolio graph',
|
|
225
|
-
description: 'Whole-graph replace: write the complete nodes + edges for a portfolio.
|
|
225
|
+
description: 'Whole-graph replace: write the complete nodes + edges for a portfolio. Use it for restructures and deletes; to ADD items use portfolio_add_items, to change one item portfolio_patch_item, to re-position portfolio_layout. Pre-flight validation rejects orphan parents, duplicate ids, and unresolvable epic links. The current server state is snapshotted (backed up) before the write. Pass expectedVersion from portfolio_get; if it no longer matches, the write is refused with a 409 instead of clobbering a concurrent edit.',
|
|
226
226
|
inputSchema: {
|
|
227
227
|
id: z.string().describe('Portfolio id.'),
|
|
228
228
|
nodes: z.array(z.any()).describe('The COMPLETE set of nodes (whole-graph replace).'),
|
|
@@ -304,9 +304,9 @@ export function registerPortfolioTools(server, ctx) {
|
|
|
304
304
|
nodes,
|
|
305
305
|
edges,
|
|
306
306
|
...(baselineVersion !== undefined ? { expectedVersion: baselineVersion } : {}),
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
...(args.viewport !== undefined ? { viewport: args.viewport } : {}),
|
|
307
|
+
name: args.name ?? String(current.name ?? current.graph?.name ?? ''),
|
|
308
|
+
description: args.description ?? String(current.description ?? current.graph?.description ?? ''),
|
|
309
|
+
...(args.viewport !== undefined ? { viewport: args.viewport } : currentGraph.viewport !== undefined ? { viewport: currentGraph.viewport } : {}),
|
|
310
310
|
}),
|
|
311
311
|
});
|
|
312
312
|
if (putRes.status === 409) {
|
package/dist/stdio.js
CHANGED
|
@@ -32,13 +32,35 @@ async function fileBackup(id, json) {
|
|
|
32
32
|
}
|
|
33
33
|
await writeFile(file, JSON.stringify(json, null, 2), 'utf8');
|
|
34
34
|
}
|
|
35
|
+
const IMAGE_MIME = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp', gif: 'image/gif' };
|
|
36
|
+
async function readLocalImage(path) {
|
|
37
|
+
const ext = path.toLowerCase().split('.').pop() ?? '';
|
|
38
|
+
const mime = IMAGE_MIME[ext];
|
|
39
|
+
if (!mime)
|
|
40
|
+
throw new Error('file_path must be a .png, .jpg, .jpeg, .webp or .gif image');
|
|
41
|
+
const buf = await readFile(resolve(path));
|
|
42
|
+
if (buf.length > 2_200_000)
|
|
43
|
+
throw new Error(`The image is ${(buf.length / 1e6).toFixed(1)} MB; the limit is 2.2 MB — downscale it first`);
|
|
44
|
+
const magic = buf.subarray(0, 12);
|
|
45
|
+
const ok = mime === 'image/png' ? magic[0] === 0x89 && magic[1] === 0x50
|
|
46
|
+
: mime === 'image/jpeg' ? magic[0] === 0xff && magic[1] === 0xd8
|
|
47
|
+
: mime === 'image/gif' ? magic.toString('latin1', 0, 3) === 'GIF'
|
|
48
|
+
: magic.toString('latin1', 0, 4) === 'RIFF' && magic.toString('latin1', 8, 12) === 'WEBP';
|
|
49
|
+
if (!ok)
|
|
50
|
+
throw new Error('file_path is not the image its extension says it is');
|
|
51
|
+
return `data:${mime};base64,${buf.toString('base64')}`;
|
|
52
|
+
}
|
|
35
53
|
async function main() {
|
|
36
|
-
const
|
|
54
|
+
const inRun = typeof process.env.NONBOT_RUN_ID === 'string' && process.env.NONBOT_RUN_ID.length > 0;
|
|
55
|
+
const token = inRun
|
|
56
|
+
? (process.env.NONBOT_PAT || '')
|
|
57
|
+
: (process.env.NON_BOT_API_TOKEN || (await tokenFromCliAuth()));
|
|
37
58
|
const ctx = {
|
|
38
59
|
apiBase: resolveApiBase(process.env.NON_BOT_API_BASE),
|
|
39
60
|
getToken: () => token,
|
|
40
61
|
readonly: process.env.NON_BOT_MCP_READONLY === '1',
|
|
41
62
|
backup: fileBackup,
|
|
63
|
+
readLocalImage,
|
|
42
64
|
};
|
|
43
65
|
const server = createNonbotMcpServer(ctx);
|
|
44
66
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nonbot/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The non.bot MCP server — one server for your portfolios, decision graph, and Runs. Runs over stdio (Claude Code, Cursor, VS Code) with a pat_ token. Built on MCP protocol 2025-11-25.",
|
|
6
6
|
"keywords": [
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@cfworker/json-schema": "^4.1.1",
|
|
58
58
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
59
|
-
"zod": "^
|
|
59
|
+
"zod": "^4.6.7"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "^22.19.19",
|