@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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +163 -22
  3. 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.1.2";
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: relatedDecisions(design, component)
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
- const seen = /* @__PURE__ */ new Set();
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 || seen.has(trimmed)) return;
1391
- seen.add(trimmed);
1392
- out.push(trimmed);
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: decisionTexts(design),
1413
- trade_offs: design.trade_offs ?? []
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, and the decisions that mention it. 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."
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
- "Decisions taken \u2014 these are settled, build to them:",
1480
- ...data.design_decisions.map((decision) => `- ${decision}`)
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
- "Decisions that mention it:",
1520
- ...slice.related_decisions.map((decision) => `- ${decision}`)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skeletiq/mcp",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Design in SkeletIQ, build with your coding agent. MCP server for the SkeletIQ architecture platform.",
5
5
  "license": "MIT",
6
6
  "author": "Sabhahith Works Private Limited",