@skeletiq/mcp 0.1.2 → 0.2.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/README.md +1 -1
- package/dist/index.js +163 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ change your provider keys, see billing, or delete your account, whatever scopes
|
|
|
85
85
|
| Tool | What it does |
|
|
86
86
|
|---|---|
|
|
87
87
|
| `list_projects` | Find a project by name. Returns the candidates rather than guessing between them. |
|
|
88
|
-
| `get_design` | Read a design, in one of six modes: `overview`, `component`, `brief`, `readiness`, `build_order`, `gaps`. |
|
|
88
|
+
| `get_design` | Read a design, in one of six modes: `overview`, `component`, `brief`, `readiness`, `build_order`, `gaps`. `overview` and `component` resolve the design's `R`-ids to the requirements they name, and what became of each. |
|
|
89
89
|
| `generate_architecture` | Design a system from a prompt. Spends credits and takes minutes. |
|
|
90
90
|
| `get_generation_status` | Poll a generation started with `wait: false`. |
|
|
91
91
|
| `critique_architecture` | Check a design against SkeletIQ's rules. Deterministic, free, stores nothing. Tell it the `domain` and the `exposure`. |
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
|
5
5
|
|
|
6
6
|
// src/config.ts
|
|
7
7
|
var SERVER_NAME = "skeletiq";
|
|
8
|
-
var SERVER_VERSION = "0.
|
|
8
|
+
var SERVER_VERSION = "0.2.0";
|
|
9
9
|
var DEFAULT_API_URL = "https://api.skeletiq.com";
|
|
10
10
|
var TOKEN_PREFIX = "skq_";
|
|
11
11
|
var ConfigError = class extends Error {
|
|
@@ -322,6 +322,40 @@ var ArchitectureJsonSchema = z.looseObject({
|
|
|
322
322
|
assumptions: z.array(GapEntrySchema).nullable().optional(),
|
|
323
323
|
open_questions: z.array(GapEntrySchema).nullable().optional()
|
|
324
324
|
});
|
|
325
|
+
var RequirementLedgerRowSchema = z.looseObject({
|
|
326
|
+
id: z.string(),
|
|
327
|
+
text: z.string().optional(),
|
|
328
|
+
kind: z.string().optional(),
|
|
329
|
+
source: z.string().optional(),
|
|
330
|
+
/** `unmet` | `uncited` | `unchecked` | `cited` — deliberately never `met`. Left as a string: a
|
|
331
|
+
* value added server-side must not stop an older connector parsing the row. */
|
|
332
|
+
status: z.string().optional(),
|
|
333
|
+
verifier_checked: z.boolean().optional(),
|
|
334
|
+
cited_by_components: z.array(z.string()).nullable().optional(),
|
|
335
|
+
/** Indices into `architecture_json.grounded_decisions`, the raw array — blanks are not compacted. */
|
|
336
|
+
cited_by_decisions: z.array(z.number()).nullable().optional(),
|
|
337
|
+
added_in_version: z.number().nullable().optional()
|
|
338
|
+
});
|
|
339
|
+
var RequirementLedgerCountsSchema = z.looseObject({
|
|
340
|
+
total: z.number().optional(),
|
|
341
|
+
user_supplied: z.number().optional(),
|
|
342
|
+
llm_inferred: z.number().optional(),
|
|
343
|
+
unmet: z.number().optional(),
|
|
344
|
+
uncited: z.number().optional(),
|
|
345
|
+
unchecked: z.number().optional(),
|
|
346
|
+
cited: z.number().optional()
|
|
347
|
+
});
|
|
348
|
+
var RequirementLedgerSchema = z.looseObject({
|
|
349
|
+
rows: z.array(RequirementLedgerRowSchema).nullable().optional(),
|
|
350
|
+
analysis_version: z.number().nullable().optional(),
|
|
351
|
+
analysis_degraded: z.boolean().optional(),
|
|
352
|
+
/** `grounded` | `not_grounded` | `diverged`. */
|
|
353
|
+
grounding_state: z.string().optional(),
|
|
354
|
+
/** `ran` | `partial` | `not_run`. */
|
|
355
|
+
verification_state: z.string().optional(),
|
|
356
|
+
verified_version: z.number().nullable().optional(),
|
|
357
|
+
counts: RequirementLedgerCountsSchema.nullable().optional()
|
|
358
|
+
});
|
|
325
359
|
var ArchitectureSchema = z.looseObject({
|
|
326
360
|
id: z.string(),
|
|
327
361
|
project_id: z.string(),
|
|
@@ -332,7 +366,8 @@ var ArchitectureSchema = z.looseObject({
|
|
|
332
366
|
created_at: z.string().optional(),
|
|
333
367
|
updated_at: z.string().optional(),
|
|
334
368
|
is_released: z.boolean().optional(),
|
|
335
|
-
released_at: z.string().nullable().optional()
|
|
369
|
+
released_at: z.string().nullable().optional(),
|
|
370
|
+
requirement_ledger: RequirementLedgerSchema.nullable().optional()
|
|
336
371
|
});
|
|
337
372
|
var ArchitecturePageSchema = z.looseObject({
|
|
338
373
|
items: z.array(ArchitectureSchema),
|
|
@@ -1369,32 +1404,63 @@ async function describeCreated(resolver, architectureId) {
|
|
|
1369
1404
|
import * as z5 from "zod/v4";
|
|
1370
1405
|
|
|
1371
1406
|
// src/lib/slice.ts
|
|
1372
|
-
function sliceComponent(design, componentId) {
|
|
1407
|
+
function sliceComponent(design, componentId, ledger) {
|
|
1373
1408
|
const components = design.components ?? [];
|
|
1374
1409
|
const component = components.find((c) => c.id === componentId) ?? components.find((c) => normalize(c.name) === normalize(componentId));
|
|
1375
1410
|
if (!component) return null;
|
|
1376
1411
|
const connections = design.connections ?? [];
|
|
1412
|
+
const related = relatedDecisions(design, component);
|
|
1377
1413
|
return {
|
|
1378
1414
|
component,
|
|
1379
1415
|
outgoing: connections.filter((c) => c.source === component.id),
|
|
1380
1416
|
incoming: connections.filter((c) => c.target === component.id),
|
|
1381
|
-
related_decisions:
|
|
1417
|
+
related_decisions: related,
|
|
1418
|
+
assumed_decisions: assumedAmong(design, related),
|
|
1419
|
+
...ledger ? { cited_requirements: citedRequirements(ledger, component.id) } : {}
|
|
1382
1420
|
};
|
|
1383
1421
|
}
|
|
1422
|
+
function citedRequirements(ledger, componentId) {
|
|
1423
|
+
return orderRequirementRows(
|
|
1424
|
+
(ledger.rows ?? []).filter((row) => (row.cited_by_components ?? []).includes(componentId))
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
var STATUS_ORDER = ["unmet", "uncited", "unchecked", "cited"];
|
|
1428
|
+
function orderRequirementRows(rows) {
|
|
1429
|
+
return [...rows].sort((a, b) => statusRank(a) - statusRank(b));
|
|
1430
|
+
}
|
|
1431
|
+
function statusRank(row) {
|
|
1432
|
+
const index = STATUS_ORDER.indexOf(row.status ?? "");
|
|
1433
|
+
return index === -1 ? STATUS_ORDER.length : index;
|
|
1434
|
+
}
|
|
1384
1435
|
function decisionTexts(design) {
|
|
1385
|
-
|
|
1436
|
+
return decisionEntries(design).map((entry) => entry.text);
|
|
1437
|
+
}
|
|
1438
|
+
function decisionEntries(design) {
|
|
1439
|
+
const byText = /* @__PURE__ */ new Map();
|
|
1386
1440
|
const out = [];
|
|
1387
|
-
const push2 = (text) => {
|
|
1441
|
+
const push2 = (text, assumption) => {
|
|
1388
1442
|
if (typeof text !== "string") return;
|
|
1389
1443
|
const trimmed = text.trim();
|
|
1390
|
-
if (trimmed.length === 0
|
|
1391
|
-
|
|
1392
|
-
|
|
1444
|
+
if (trimmed.length === 0) return;
|
|
1445
|
+
const existing = byText.get(trimmed);
|
|
1446
|
+
if (existing) {
|
|
1447
|
+
existing.assumption = existing.assumption || assumption;
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
const entry = { text: trimmed, assumption };
|
|
1451
|
+
byText.set(trimmed, entry);
|
|
1452
|
+
out.push(entry);
|
|
1393
1453
|
};
|
|
1394
|
-
for (const decision of design.design_decisions ?? []) push2(decision);
|
|
1395
|
-
for (const decision of design.grounded_decisions ?? []) push2(decision?.text);
|
|
1454
|
+
for (const decision of design.design_decisions ?? []) push2(decision, false);
|
|
1455
|
+
for (const decision of design.grounded_decisions ?? []) push2(decision?.text, decision?.assumption === true);
|
|
1396
1456
|
return out;
|
|
1397
1457
|
}
|
|
1458
|
+
function assumedAmong(design, texts) {
|
|
1459
|
+
const assumed = new Set(
|
|
1460
|
+
decisionEntries(design).filter((entry) => entry.assumption).map((entry) => entry.text)
|
|
1461
|
+
);
|
|
1462
|
+
return texts.filter((text) => assumed.has(text));
|
|
1463
|
+
}
|
|
1398
1464
|
function relatedDecisions(design, component) {
|
|
1399
1465
|
const needles = [component.name, component.technology].filter((value) => typeof value === "string" && value.trim().length > 2).map((value) => value.toLowerCase());
|
|
1400
1466
|
if (needles.length === 0) return [];
|
|
@@ -1403,14 +1469,24 @@ function relatedDecisions(design, component) {
|
|
|
1403
1469
|
return needles.some((needle) => haystack.includes(needle));
|
|
1404
1470
|
});
|
|
1405
1471
|
}
|
|
1406
|
-
function overview(design) {
|
|
1472
|
+
function overview(design, ledger) {
|
|
1473
|
+
const decisions = decisionTexts(design);
|
|
1407
1474
|
return {
|
|
1408
1475
|
title: design.title ?? "Untitled design",
|
|
1409
1476
|
description: design.description ?? "",
|
|
1410
1477
|
components: design.components ?? [],
|
|
1411
1478
|
connections: design.connections ?? [],
|
|
1412
|
-
design_decisions:
|
|
1413
|
-
|
|
1479
|
+
design_decisions: decisions,
|
|
1480
|
+
// A subset of `design_decisions`, not a second list of decisions: which of them the design
|
|
1481
|
+
// marks as resting on no cited source. Additive, so `design_decisions` stays the plain
|
|
1482
|
+
// strings every existing caller reads.
|
|
1483
|
+
assumed_decisions: assumedAmong(design, decisions),
|
|
1484
|
+
trade_offs: design.trade_offs ?? [],
|
|
1485
|
+
// Carried verbatim, rows in the server's order, and only when the version has one. This is
|
|
1486
|
+
// a rebuild from named keys, so anything not named here is dropped without a trace — which
|
|
1487
|
+
// is how `grounded_decisions` went missing for two releases. Re-sorting the rows on the way
|
|
1488
|
+
// past would make "verbatim" untrue; the grouping belongs to the text that prints them.
|
|
1489
|
+
...ledger ? { requirement_ledger: ledger } : {}
|
|
1414
1490
|
};
|
|
1415
1491
|
}
|
|
1416
1492
|
function normalize(value) {
|
|
@@ -1422,7 +1498,7 @@ var MODES = ["overview", "component", "brief", "readiness", "build_order", "gaps
|
|
|
1422
1498
|
var input3 = z5.object({
|
|
1423
1499
|
project_id: z5.string().describe("A SkeletIQ project id, or its exact name."),
|
|
1424
1500
|
mode: z5.enum(MODES).describe(
|
|
1425
|
-
"overview: the whole design. component: one component, its connections in both directions,
|
|
1501
|
+
"overview: the whole design, and the requirements it was built from with what became of each. component: one component, its connections in both directions, the decisions that mention it, and the requirements the design cites it for. brief: the fenced markdown block to write into AGENTS.md. readiness: what still has to be decided before this design is worth building. build_order: the order to build components in, and why. gaps: the open questions and unconfirmed assumptions in the design."
|
|
1426
1502
|
),
|
|
1427
1503
|
component_id: z5.string().optional().describe('Required for mode "component". The component id, or its name.'),
|
|
1428
1504
|
version: z5.number().int().positive().optional().describe("A specific version. Defaults to the latest released version, or the latest version if none is released.")
|
|
@@ -1438,12 +1514,19 @@ var output3 = z5.object({
|
|
|
1438
1514
|
/** Mode-shaped. Deliberately loose: six modes cannot share one strict schema honestly. */
|
|
1439
1515
|
data: z5.unknown()
|
|
1440
1516
|
});
|
|
1517
|
+
function decisionsHeading(assumed, settled, withAssumptions) {
|
|
1518
|
+
return assumed.length > 0 ? withAssumptions : settled;
|
|
1519
|
+
}
|
|
1520
|
+
function decisionLines(decisions, assumed) {
|
|
1521
|
+
const marked = new Set(assumed);
|
|
1522
|
+
return decisions.map((decision) => `- ${decision}${marked.has(decision) ? " (assumption)" : ""}`);
|
|
1523
|
+
}
|
|
1441
1524
|
function registerGetDesign(server, client, resolver) {
|
|
1442
1525
|
server.registerTool(
|
|
1443
1526
|
"get_design",
|
|
1444
1527
|
{
|
|
1445
1528
|
title: "Read a SkeletIQ design",
|
|
1446
|
-
description: 'Read a SkeletIQ architecture. Start with mode "brief" to orient \u2014 it returns a fenced block to write into AGENTS.md \u2014 then "build_order" for the sequence and "component" for each piece as you build it. Every answer states which version it came from and whether that version is a release.',
|
|
1529
|
+
description: 'Read a SkeletIQ architecture. Start with mode "brief" to orient \u2014 it returns a fenced block to write into AGENTS.md \u2014 then "build_order" for the sequence and "component" for each piece as you build it. "overview" and "component" also resolve the R-ids the design carries to the requirements they name, and say what became of each. Every answer states which version it came from and whether that version is a release.',
|
|
1447
1530
|
inputSchema: input3,
|
|
1448
1531
|
outputSchema: output3,
|
|
1449
1532
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
|
|
@@ -1459,7 +1542,7 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1459
1542
|
switch (mode) {
|
|
1460
1543
|
case "overview": {
|
|
1461
1544
|
const design = await resolver.design(resolved.architectureId);
|
|
1462
|
-
const data = overview(design.architecture_json);
|
|
1545
|
+
const data = overview(design.architecture_json, design.requirement_ledger);
|
|
1463
1546
|
const facts = factsFrom(resolved);
|
|
1464
1547
|
return ok({ ...base, ...facts, data }, [
|
|
1465
1548
|
`# ${data.title}`,
|
|
@@ -1469,6 +1552,10 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1469
1552
|
...data.components.map(
|
|
1470
1553
|
(c) => `- ${c.id} \u2014 ${c.name} (${c.type}${c.technology ? `, ${c.technology}` : ""})`
|
|
1471
1554
|
),
|
|
1555
|
+
// Above the decisions because the two answer different questions, in
|
|
1556
|
+
// this order: requirements are what was asked for, decisions are how
|
|
1557
|
+
// the design answered.
|
|
1558
|
+
...requirementBlock(data.requirement_ledger),
|
|
1472
1559
|
// The design's own reasoning, which this text used to omit entirely
|
|
1473
1560
|
// while carrying it in the structured half — so an agent working from
|
|
1474
1561
|
// what it was shown rebuilt every choice from scratch, and could
|
|
@@ -1476,8 +1563,12 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1476
1563
|
// like this", and the overview is where that gets asked.
|
|
1477
1564
|
...data.design_decisions.length ? [
|
|
1478
1565
|
"",
|
|
1479
|
-
|
|
1480
|
-
|
|
1566
|
+
decisionsHeading(
|
|
1567
|
+
data.assumed_decisions,
|
|
1568
|
+
"Decisions taken \u2014 these are settled, build to them:",
|
|
1569
|
+
"Decisions taken \u2014 build to them; confirm the ones marked (assumption) first, they rest on no cited source:"
|
|
1570
|
+
),
|
|
1571
|
+
...decisionLines(data.design_decisions, data.assumed_decisions)
|
|
1481
1572
|
] : [],
|
|
1482
1573
|
...data.trade_offs.length ? [
|
|
1483
1574
|
"",
|
|
@@ -1493,7 +1584,7 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1493
1584
|
return failure('mode "component" needs a component_id. Use mode "overview" or "build_order" to see the ids.');
|
|
1494
1585
|
}
|
|
1495
1586
|
const design = await resolver.design(resolved.architectureId);
|
|
1496
|
-
const slice = sliceComponent(design.architecture_json, component_id);
|
|
1587
|
+
const slice = sliceComponent(design.architecture_json, component_id, design.requirement_ledger);
|
|
1497
1588
|
if (!slice) {
|
|
1498
1589
|
const known = (design.architecture_json.components ?? []).map((c) => c.id).join(", ");
|
|
1499
1590
|
return failure(
|
|
@@ -1509,6 +1600,9 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1509
1600
|
"",
|
|
1510
1601
|
outgoing.length ? `Calls: ${outgoing.map((c) => `${c.target}${c.protocol ? ` (${c.protocol})` : ""}`).join(", ")}` : "Calls nothing.",
|
|
1511
1602
|
incoming.length ? `Called by: ${incoming.map((c) => c.source).join(", ")}` : "Called by nothing in the design.",
|
|
1603
|
+
// Same order as the overview: what was asked for, then how the design
|
|
1604
|
+
// answered.
|
|
1605
|
+
...requirementsServed(slice.cited_requirements),
|
|
1512
1606
|
// Labelled, and only when there are any. These bullets used to be
|
|
1513
1607
|
// printed bare under "Called by:", so a decision about the component
|
|
1514
1608
|
// read as a continuation of its edge list — and a design with no
|
|
@@ -1516,8 +1610,12 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1516
1610
|
// with no reasoning rather than as a decision that names other boxes.
|
|
1517
1611
|
...slice.related_decisions.length ? [
|
|
1518
1612
|
"",
|
|
1519
|
-
|
|
1520
|
-
|
|
1613
|
+
decisionsHeading(
|
|
1614
|
+
slice.assumed_decisions,
|
|
1615
|
+
"Decisions that mention it:",
|
|
1616
|
+
"Decisions that mention it \u2014 confirm the ones marked (assumption) first, they rest on no cited source:"
|
|
1617
|
+
),
|
|
1618
|
+
...decisionLines(slice.related_decisions, slice.assumed_decisions)
|
|
1521
1619
|
] : [
|
|
1522
1620
|
"",
|
|
1523
1621
|
"No decision in this design names this component. That is not the same as",
|
|
@@ -1644,6 +1742,49 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1644
1742
|
})
|
|
1645
1743
|
);
|
|
1646
1744
|
}
|
|
1745
|
+
function requirementBlock(ledger) {
|
|
1746
|
+
const rows = orderRequirementRows(ledger?.rows ?? []);
|
|
1747
|
+
if (!ledger || rows.length === 0) return [];
|
|
1748
|
+
const attention = ["unmet", "uncited", "unchecked"].map((status) => [rows.filter((row) => row.status === status).length, status]).filter(([count]) => count > 0).map(([count, status]) => `${count} ${status}`);
|
|
1749
|
+
return [
|
|
1750
|
+
"",
|
|
1751
|
+
`Requirements \u2014 ${rows.length} total${attention.length ? `; ${attention.join(", ")}` : ""}:`,
|
|
1752
|
+
...rows.map(requirementLine),
|
|
1753
|
+
"Statuses: `unmet` \u2014 the verifier judged the design does not satisfy it; `uncited` \u2014 nothing",
|
|
1754
|
+
"in the design claims to address it; `unchecked` \u2014 something cites it and the verifier did not",
|
|
1755
|
+
"check it; `cited` \u2014 cited with no gap found, which is not the same as proven.",
|
|
1756
|
+
// Two facts about how far the statuses can be trusted, said only when the server states
|
|
1757
|
+
// them. An absent `verification_state` is an older server, not a verifier that did not run,
|
|
1758
|
+
// and claiming the second would be this package inventing a fact about someone's design.
|
|
1759
|
+
...ledger.grounding_state === "not_grounded" ? [
|
|
1760
|
+
"This version records no citations at all, so `uncited` here means nothing recorded what",
|
|
1761
|
+
"addresses the requirement \u2014 not that nothing in the design does."
|
|
1762
|
+
] : [],
|
|
1763
|
+
...ledger.verification_state === "partial" ? [
|
|
1764
|
+
"The verifier did not check every requirement in this set, so an `unchecked` row is",
|
|
1765
|
+
"neither confirmed nor flagged."
|
|
1766
|
+
] : ledger.verification_state === "not_run" ? [
|
|
1767
|
+
"The verifier did not run on this version, so an `unchecked` row is neither confirmed",
|
|
1768
|
+
"nor flagged."
|
|
1769
|
+
] : [],
|
|
1770
|
+
...ledger.analysis_degraded ? ["The requirements analysis for this version ran degraded. Treat the list as incomplete."] : []
|
|
1771
|
+
];
|
|
1772
|
+
}
|
|
1773
|
+
function requirementsServed(rows) {
|
|
1774
|
+
if (!rows?.length) return [];
|
|
1775
|
+
return [
|
|
1776
|
+
"",
|
|
1777
|
+
"Requirements the design cites this component for:",
|
|
1778
|
+
...rows.map(requirementLine),
|
|
1779
|
+
"`unmet` \u2014 the verifier judged the design does not satisfy it; `unchecked` \u2014 the verifier did",
|
|
1780
|
+
"not check it; `cited` \u2014 no gap found, which is not the same as proven."
|
|
1781
|
+
];
|
|
1782
|
+
}
|
|
1783
|
+
function requirementLine(row) {
|
|
1784
|
+
const status = row.status ? ` [${row.status}]` : "";
|
|
1785
|
+
const text = (row.text ?? "").replace(/\s+/g, " ").trim();
|
|
1786
|
+
return `- ${row.id}${status} ${text || "(no text recorded for this requirement)"}`;
|
|
1787
|
+
}
|
|
1647
1788
|
function settledAnswer(gap) {
|
|
1648
1789
|
const note = (gap.note ?? "").trim();
|
|
1649
1790
|
if (gap.adr_id) {
|
package/package.json
CHANGED