@skeletiq/mcp 0.1.0 → 0.1.1

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 +51 -4
  2. package/dist/index.js +94 -6
  3. package/package.json +8 -1
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # @skeletiq/mcp
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@skeletiq/mcp)](https://www.npmjs.com/package/@skeletiq/mcp)
4
+ [![licence](https://img.shields.io/npm/l/@skeletiq/mcp)](./LICENSE)
5
+ [![node](https://img.shields.io/node/v/@skeletiq/mcp)](https://nodejs.org)
6
+ [![CI](https://github.com/Sabhahith-Works/skeletiq-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Sabhahith-Works/skeletiq-mcp/actions/workflows/ci.yml)
7
+
3
8
  Design in [SkeletIQ](https://skeletiq.com), build with your coding agent.
4
9
 
5
10
  SkeletIQ turns a prompt into a critiqued system architecture — components, data stores,
@@ -58,11 +63,20 @@ A token grants only what you tick. The server's tools need:
58
63
  | Scope | What it unlocks | Tools |
59
64
  |---|---|---|
60
65
  | `read` | Projects, designs, briefs, build order, readiness, gaps, jobs | `list_projects`, `get_design`, `get_generation_status` |
61
- | `generate` | Running generations and payload critique. **Spends credits.** | `generate_architecture`, `critique_architecture` |
62
- | `report` | Recording what got built | `check_drift` |
66
+ | `generate` | Running generations — **that spends credits** — and payload critique, which is free | `generate_architecture`, `critique_architecture` |
67
+ | `report` | Recording what got built. **Needs `read` as well** | `check_drift` |
63
68
 
64
69
  `read` alone is a good starting point: the agent can orient and build, but cannot spend anything.
65
70
 
71
+ Two things the table above cannot say in a cell:
72
+
73
+ - **`report` on its own does nothing.** `check_drift` looks the project and the version up before it
74
+ can report against them, and those lookups are `read`. A `report`-only token is refused.
75
+ - **`generate` without `read` still generates**, but the answer is thinner: the tool reads back the
76
+ design it just created to describe it, and reports that failure as a thinner answer rather than a
77
+ failed generation — telling an agent the generation failed would invite it to pay for the whole
78
+ thing again.
79
+
66
80
  Everything else is out of reach by construction — a token cannot mint another token, read or
67
81
  change your provider keys, see billing, or delete your account, whatever scopes it carries.
68
82
 
@@ -112,7 +126,40 @@ warns when a default was applied. Read those before reporting a score to a perso
112
126
  Whichever one the account holder chose under **Settings → Agent access**. The tools take no runtime
113
127
  argument, deliberately: the model asking for a design does not get to choose what it costs you.
114
128
 
129
+ ## Development
130
+
131
+ This repository is the source of the published `@skeletiq/mcp` package. The connector is developed
132
+ in SkeletIQ's monorepo, alongside the API it talks to, and mirrored here — so the history you see is
133
+ the package's real history, not a squashed snapshot. A pull request opened here cannot be merged,
134
+ because the next sync would overwrite it; [CONTRIBUTING.md](./CONTRIBUTING.md) explains what to do
135
+ instead.
136
+
137
+ Node 20 or newer.
138
+
139
+ ```bash
140
+ npm install
141
+ npm test # vitest — hermetic: no network, no services, nothing to seed
142
+ npm run build # tsup, to dist/index.js
143
+ npm run typecheck
144
+ npm run lint
145
+ ```
146
+
147
+ The tests mock the SkeletIQ API rather than calling it, so a clean clone runs them without a token
148
+ and without an account.
149
+
150
+ **If `npm install` fails with `Cannot read properties of null (reading 'edgesOut')`,** you are on npm
151
+ 10.9.x — the version Node 22 ships — which cannot resolve this tree; `vitest@4` alone triggers it.
152
+ `npm install -g npm@11` fixes it. This affects cloning and building only: installing the published
153
+ package with `npx` works on that npm.
154
+
115
155
  ## Licence
116
156
 
117
- MIT — see [LICENSE](./LICENSE). The rest of the SkeletIQ repository is AGPL-3.0-or-later; this
118
- connector is MIT so it can be embedded, vendored and forked freely.
157
+ MIT — see [LICENSE](./LICENSE). Source:
158
+ [Sabhahith-Works/skeletiq-mcp](https://github.com/Sabhahith-Works/skeletiq-mcp) issues and questions
159
+ go [there](https://github.com/Sabhahith-Works/skeletiq-mcp/issues).
160
+
161
+ The SkeletIQ platform is AGPL-3.0-or-later; this connector is MIT so it can be embedded, vendored
162
+ and forked freely.
163
+
164
+ "SkeletIQ" is a mark of Sabhahith Works Private Limited — see [NOTICE](./NOTICE). Security reports go
165
+ to security@skeletiq.com, not to the issue tracker: [SECURITY.md](./SECURITY.md).
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.0";
8
+ var SERVER_VERSION = "0.1.1";
9
9
  var DEFAULT_API_URL = "https://api.skeletiq.com";
10
10
  var TOKEN_PREFIX = "skq_";
11
11
  var ConfigError = class extends Error {
@@ -303,12 +303,20 @@ var GapEntrySchema = z.union([
303
303
  impact: z.string().nullable().optional()
304
304
  })
305
305
  ]);
306
+ var GroundedDecisionSchema = z.looseObject({
307
+ text: z.string(),
308
+ source_ids: z.array(z.string()).nullable().optional(),
309
+ requirement_ids: z.array(z.string()).nullable().optional(),
310
+ assumption: z.boolean().optional(),
311
+ confidence: z.string().optional()
312
+ });
306
313
  var ArchitectureJsonSchema = z.looseObject({
307
314
  title: z.string().optional(),
308
315
  description: z.string().optional(),
309
316
  components: z.array(ComponentSchema).optional(),
310
317
  connections: z.array(ConnectionSchema).optional(),
311
318
  design_decisions: z.array(z.string()).nullable().optional(),
319
+ grounded_decisions: z.array(GroundedDecisionSchema).nullable().optional(),
312
320
  trade_offs: z.array(z.string()).nullable().optional(),
313
321
  scalability_notes: z.string().nullable().optional(),
314
322
  assumptions: z.array(GapEntrySchema).nullable().optional(),
@@ -387,7 +395,15 @@ var ReadinessSchema = z.looseObject({
387
395
  /** Keys of advisory rows whose check never ran. Named, never counted. */
388
396
  unrun_checks: z.array(z.string()).optional(),
389
397
  rows: z.array(ReadinessRowSchema),
390
- release: ReleaseFactsSchema
398
+ release: ReleaseFactsSchema,
399
+ /**
400
+ * What releasing this version would be carrying, one sentence each. A warning, never a refusal.
401
+ *
402
+ * The same list the app shows beside its Release button, under the heading "It will carry:".
403
+ * Optional here only so a server that predates the field still parses — it is required on the
404
+ * wire, and an empty array is the server saying it looked and found nothing.
405
+ */
406
+ release_warnings: z.array(z.string()).optional()
391
407
  });
392
408
  var DesignGapSchema = z.looseObject({
393
409
  gap_id: z.string(),
@@ -398,12 +414,21 @@ var DesignGapSchema = z.looseObject({
398
414
  note: z.string().nullable().optional(),
399
415
  adr_id: z.string().nullable().optional()
400
416
  });
417
+ var OrphanedAnswerSchema = z.looseObject({
418
+ gap_id: z.string(),
419
+ kind: z.string(),
420
+ action: z.string(),
421
+ note: z.string().nullable().optional(),
422
+ adr_id: z.string().nullable().optional()
423
+ });
401
424
  var DesignGapListSchema = z.looseObject({
402
425
  project_id: z.string(),
403
426
  architecture_id: z.string().nullable().optional(),
404
427
  version: z.number().nullable().optional(),
405
428
  gaps: z.array(DesignGapSchema),
406
- unresolved_count: z.number()
429
+ unresolved_count: z.number(),
430
+ /** Answers whose question is not in this version — recorded, not silently dropped. */
431
+ orphaned_answers: z.array(OrphanedAnswerSchema).optional()
407
432
  });
408
433
  var ComponentRefSchema = z.looseObject({
409
434
  id: z.string(),
@@ -1349,10 +1374,24 @@ function sliceComponent(design, componentId) {
1349
1374
  related_decisions: relatedDecisions(design, component)
1350
1375
  };
1351
1376
  }
1377
+ function decisionTexts(design) {
1378
+ const seen = /* @__PURE__ */ new Set();
1379
+ const out = [];
1380
+ const push2 = (text) => {
1381
+ if (typeof text !== "string") return;
1382
+ const trimmed = text.trim();
1383
+ if (trimmed.length === 0 || seen.has(trimmed)) return;
1384
+ seen.add(trimmed);
1385
+ out.push(trimmed);
1386
+ };
1387
+ for (const decision of design.design_decisions ?? []) push2(decision);
1388
+ for (const decision of design.grounded_decisions ?? []) push2(decision?.text);
1389
+ return out;
1390
+ }
1352
1391
  function relatedDecisions(design, component) {
1353
1392
  const needles = [component.name, component.technology].filter((value) => typeof value === "string" && value.trim().length > 2).map((value) => value.toLowerCase());
1354
1393
  if (needles.length === 0) return [];
1355
- return (design.design_decisions ?? []).filter((decision) => {
1394
+ return decisionTexts(design).filter((decision) => {
1356
1395
  const haystack = decision.toLowerCase();
1357
1396
  return needles.some((needle) => haystack.includes(needle));
1358
1397
  });
@@ -1363,7 +1402,7 @@ function overview(design) {
1363
1402
  description: design.description ?? "",
1364
1403
  components: design.components ?? [],
1365
1404
  connections: design.connections ?? [],
1366
- design_decisions: design.design_decisions ?? [],
1405
+ design_decisions: decisionTexts(design),
1367
1406
  trade_offs: design.trade_offs ?? []
1368
1407
  };
1369
1408
  }
@@ -1423,6 +1462,21 @@ function registerGetDesign(server, client, resolver) {
1423
1462
  ...data.components.map(
1424
1463
  (c) => `- ${c.id} \u2014 ${c.name} (${c.type}${c.technology ? `, ${c.technology}` : ""})`
1425
1464
  ),
1465
+ // The design's own reasoning, which this text used to omit entirely
1466
+ // while carrying it in the structured half — so an agent working from
1467
+ // what it was shown rebuilt every choice from scratch, and could
1468
+ // silently undo one. Said here because it is the answer to "why is it
1469
+ // like this", and the overview is where that gets asked.
1470
+ ...data.design_decisions.length ? [
1471
+ "",
1472
+ "Decisions taken \u2014 these are settled, build to them:",
1473
+ ...data.design_decisions.map((decision) => `- ${decision}`)
1474
+ ] : [],
1475
+ ...data.trade_offs.length ? [
1476
+ "",
1477
+ "Trade-offs the design accepts:",
1478
+ ...data.trade_offs.map((tradeOff) => `- ${tradeOff}`)
1479
+ ] : [],
1426
1480
  "",
1427
1481
  versionLine(facts)
1428
1482
  ].join("\n"));
@@ -1448,7 +1502,20 @@ function registerGetDesign(server, client, resolver) {
1448
1502
  "",
1449
1503
  outgoing.length ? `Calls: ${outgoing.map((c) => `${c.target}${c.protocol ? ` (${c.protocol})` : ""}`).join(", ")}` : "Calls nothing.",
1450
1504
  incoming.length ? `Called by: ${incoming.map((c) => c.source).join(", ")}` : "Called by nothing in the design.",
1451
- ...slice.related_decisions.map((decision) => `- ${decision}`),
1505
+ // Labelled, and only when there are any. These bullets used to be
1506
+ // printed bare under "Called by:", so a decision about the component
1507
+ // read as a continuation of its edge list — and a design with no
1508
+ // matching decision printed nothing at all, which reads as a design
1509
+ // with no reasoning rather than as a decision that names other boxes.
1510
+ ...slice.related_decisions.length ? [
1511
+ "",
1512
+ "Decisions that mention it:",
1513
+ ...slice.related_decisions.map((decision) => `- ${decision}`)
1514
+ ] : [
1515
+ "",
1516
+ "No decision in this design names this component. That is not the same as",
1517
+ `no reason \u2014 use mode "overview" for the design's decisions in full.`
1518
+ ],
1452
1519
  "",
1453
1520
  versionLine(facts)
1454
1521
  ].join("\n"));
@@ -1500,6 +1567,16 @@ function registerGetDesign(server, client, resolver) {
1500
1567
  ...advisories.map(
1501
1568
  (row) => `- ${row.label}: ${describeRow(row)}${unrun.has(row.key) ? " (never run on this version)" : ""}`
1502
1569
  ),
1570
+ // The same list, under the same heading, as the app shows beside its
1571
+ // Release button. A person clicking Release is told what the release
1572
+ // takes with it; an agent asking the same question of the same version
1573
+ // was told nothing at all. A warning, never a refusal — it does not
1574
+ // move the verdict and it does not stop anyone.
1575
+ ...readiness.release_warnings?.length ? [
1576
+ "",
1577
+ "It will carry:",
1578
+ ...readiness.release_warnings.map((warning) => `- ${warning}`)
1579
+ ] : [],
1503
1580
  "",
1504
1581
  "Only a person can clear a gate. Ask; do not decide on their behalf.",
1505
1582
  versionLine(facts)
@@ -1530,6 +1607,7 @@ function registerGetDesign(server, client, resolver) {
1530
1607
  const facts = factsFrom(resolved);
1531
1608
  const unresolved = gaps.gaps.filter((gap) => !gap.resolved);
1532
1609
  const settled = gaps.gaps.filter((gap) => gap.resolved && gap.action !== "dismissed");
1610
+ const orphaned = gaps.orphaned_answers ?? [];
1533
1611
  return ok({ ...base, ...facts, data: gaps }, [
1534
1612
  unresolved.length === 0 ? "Every open question and assumption in this design has been dealt with." : `${unresolved.length} unresolved:`,
1535
1613
  ...unresolved.map((gap) => `- [${gap.kind}] ${gap.text}`),
@@ -1538,6 +1616,16 @@ function registerGetDesign(server, client, resolver) {
1538
1616
  `${settled.length} settled during review \u2014 treat these as decided:`,
1539
1617
  ...settled.map((gap) => `- [${gap.kind}] ${gap.text} \u2192 ${settledAnswer(gap)}`)
1540
1618
  ] : [],
1619
+ // A gap is identified by a hash of its own text, so a regeneration that
1620
+ // re-words a question strands the answer that settled it. The server
1621
+ // reports those rather than dropping them, and then this dropped them.
1622
+ // A count and a sentence, not rows: the stored answer has no text to
1623
+ // show, and it is not work anyone can pick up.
1624
+ ...orphaned.length ? [
1625
+ "",
1626
+ `${orphaned.length} earlier answer(s) do not match any question in this version \u2014 re-worded or dropped when it was generated, or asked only in a different version. Any Decision they minted still stands.`,
1627
+ ...orphaned.filter((answer) => answer.adr_id).map((answer) => `- [${answer.kind}] ${answer.action} \u2192 Decision ${answer.adr_id}`)
1628
+ ] : [],
1541
1629
  "",
1542
1630
  "The unresolved ones are questions for a person, not for you to answer. Raise",
1543
1631
  "them; do not guess and build on the guess. An API token cannot resolve them \u2014",
package/package.json CHANGED
@@ -1,10 +1,17 @@
1
1
  {
2
2
  "name": "@skeletiq/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",
7
7
  "homepage": "https://skeletiq.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Sabhahith-Works/skeletiq-mcp.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/Sabhahith-Works/skeletiq-mcp/issues"
14
+ },
8
15
  "keywords": [
9
16
  "mcp",
10
17
  "modelcontextprotocol",