@skeletiq/mcp 0.1.0 → 0.1.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 +51 -4
- package/dist/index.js +102 -7
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @skeletiq/mcp
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@skeletiq/mcp)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](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
|
|
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).
|
|
118
|
-
|
|
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.
|
|
8
|
+
var SERVER_VERSION = "0.1.2";
|
|
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(),
|
|
@@ -1012,7 +1037,14 @@ function frameDetail(frame) {
|
|
|
1012
1037
|
var constraintsInput = z4.strictObject({
|
|
1013
1038
|
budget: z4.enum(["low", "medium", "high", "unlimited"]).optional(),
|
|
1014
1039
|
max_latency_ms: z4.number().int().positive().optional(),
|
|
1015
|
-
|
|
1040
|
+
// Free strings, not an enum, and the description has to say why. SkeletIQ assesses five
|
|
1041
|
+
// frameworks; naming anything else is still worth doing, because an unassessed regime is
|
|
1042
|
+
// recorded and reaches the design brief as a requirement the agent still has to satisfy.
|
|
1043
|
+
// The old text read 'e.g. HIPAA, SOC2, PCI-DSS, GDPR' — it omitted SOX, which is the one
|
|
1044
|
+
// an agent is least likely to guess, and said nothing about what happens to the rest.
|
|
1045
|
+
compliance: z4.array(z4.string()).optional().describe(
|
|
1046
|
+
"Regimes the design must satisfy. SOC2, HIPAA, PCI-DSS, GDPR and SOX are checked against requirement catalogues; any other regime is recorded and carried into the design brief as a requirement to satisfy, rather than checked."
|
|
1047
|
+
),
|
|
1016
1048
|
preferred_cloud: z4.enum(["aws", "gcp", "azure", "any"]).optional(),
|
|
1017
1049
|
preferred_technologies: z4.array(z4.string()).optional(),
|
|
1018
1050
|
max_components: z4.number().int().positive().optional(),
|
|
@@ -1349,10 +1381,24 @@ function sliceComponent(design, componentId) {
|
|
|
1349
1381
|
related_decisions: relatedDecisions(design, component)
|
|
1350
1382
|
};
|
|
1351
1383
|
}
|
|
1384
|
+
function decisionTexts(design) {
|
|
1385
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1386
|
+
const out = [];
|
|
1387
|
+
const push2 = (text) => {
|
|
1388
|
+
if (typeof text !== "string") return;
|
|
1389
|
+
const trimmed = text.trim();
|
|
1390
|
+
if (trimmed.length === 0 || seen.has(trimmed)) return;
|
|
1391
|
+
seen.add(trimmed);
|
|
1392
|
+
out.push(trimmed);
|
|
1393
|
+
};
|
|
1394
|
+
for (const decision of design.design_decisions ?? []) push2(decision);
|
|
1395
|
+
for (const decision of design.grounded_decisions ?? []) push2(decision?.text);
|
|
1396
|
+
return out;
|
|
1397
|
+
}
|
|
1352
1398
|
function relatedDecisions(design, component) {
|
|
1353
1399
|
const needles = [component.name, component.technology].filter((value) => typeof value === "string" && value.trim().length > 2).map((value) => value.toLowerCase());
|
|
1354
1400
|
if (needles.length === 0) return [];
|
|
1355
|
-
return (design
|
|
1401
|
+
return decisionTexts(design).filter((decision) => {
|
|
1356
1402
|
const haystack = decision.toLowerCase();
|
|
1357
1403
|
return needles.some((needle) => haystack.includes(needle));
|
|
1358
1404
|
});
|
|
@@ -1363,7 +1409,7 @@ function overview(design) {
|
|
|
1363
1409
|
description: design.description ?? "",
|
|
1364
1410
|
components: design.components ?? [],
|
|
1365
1411
|
connections: design.connections ?? [],
|
|
1366
|
-
design_decisions: design
|
|
1412
|
+
design_decisions: decisionTexts(design),
|
|
1367
1413
|
trade_offs: design.trade_offs ?? []
|
|
1368
1414
|
};
|
|
1369
1415
|
}
|
|
@@ -1423,6 +1469,21 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1423
1469
|
...data.components.map(
|
|
1424
1470
|
(c) => `- ${c.id} \u2014 ${c.name} (${c.type}${c.technology ? `, ${c.technology}` : ""})`
|
|
1425
1471
|
),
|
|
1472
|
+
// The design's own reasoning, which this text used to omit entirely
|
|
1473
|
+
// while carrying it in the structured half — so an agent working from
|
|
1474
|
+
// what it was shown rebuilt every choice from scratch, and could
|
|
1475
|
+
// silently undo one. Said here because it is the answer to "why is it
|
|
1476
|
+
// like this", and the overview is where that gets asked.
|
|
1477
|
+
...data.design_decisions.length ? [
|
|
1478
|
+
"",
|
|
1479
|
+
"Decisions taken \u2014 these are settled, build to them:",
|
|
1480
|
+
...data.design_decisions.map((decision) => `- ${decision}`)
|
|
1481
|
+
] : [],
|
|
1482
|
+
...data.trade_offs.length ? [
|
|
1483
|
+
"",
|
|
1484
|
+
"Trade-offs the design accepts:",
|
|
1485
|
+
...data.trade_offs.map((tradeOff) => `- ${tradeOff}`)
|
|
1486
|
+
] : [],
|
|
1426
1487
|
"",
|
|
1427
1488
|
versionLine(facts)
|
|
1428
1489
|
].join("\n"));
|
|
@@ -1448,7 +1509,20 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1448
1509
|
"",
|
|
1449
1510
|
outgoing.length ? `Calls: ${outgoing.map((c) => `${c.target}${c.protocol ? ` (${c.protocol})` : ""}`).join(", ")}` : "Calls nothing.",
|
|
1450
1511
|
incoming.length ? `Called by: ${incoming.map((c) => c.source).join(", ")}` : "Called by nothing in the design.",
|
|
1451
|
-
|
|
1512
|
+
// Labelled, and only when there are any. These bullets used to be
|
|
1513
|
+
// printed bare under "Called by:", so a decision about the component
|
|
1514
|
+
// read as a continuation of its edge list — and a design with no
|
|
1515
|
+
// matching decision printed nothing at all, which reads as a design
|
|
1516
|
+
// with no reasoning rather than as a decision that names other boxes.
|
|
1517
|
+
...slice.related_decisions.length ? [
|
|
1518
|
+
"",
|
|
1519
|
+
"Decisions that mention it:",
|
|
1520
|
+
...slice.related_decisions.map((decision) => `- ${decision}`)
|
|
1521
|
+
] : [
|
|
1522
|
+
"",
|
|
1523
|
+
"No decision in this design names this component. That is not the same as",
|
|
1524
|
+
`no reason \u2014 use mode "overview" for the design's decisions in full.`
|
|
1525
|
+
],
|
|
1452
1526
|
"",
|
|
1453
1527
|
versionLine(facts)
|
|
1454
1528
|
].join("\n"));
|
|
@@ -1500,6 +1574,16 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1500
1574
|
...advisories.map(
|
|
1501
1575
|
(row) => `- ${row.label}: ${describeRow(row)}${unrun.has(row.key) ? " (never run on this version)" : ""}`
|
|
1502
1576
|
),
|
|
1577
|
+
// The same list, under the same heading, as the app shows beside its
|
|
1578
|
+
// Release button. A person clicking Release is told what the release
|
|
1579
|
+
// takes with it; an agent asking the same question of the same version
|
|
1580
|
+
// was told nothing at all. A warning, never a refusal — it does not
|
|
1581
|
+
// move the verdict and it does not stop anyone.
|
|
1582
|
+
...readiness.release_warnings?.length ? [
|
|
1583
|
+
"",
|
|
1584
|
+
"It will carry:",
|
|
1585
|
+
...readiness.release_warnings.map((warning) => `- ${warning}`)
|
|
1586
|
+
] : [],
|
|
1503
1587
|
"",
|
|
1504
1588
|
"Only a person can clear a gate. Ask; do not decide on their behalf.",
|
|
1505
1589
|
versionLine(facts)
|
|
@@ -1530,6 +1614,7 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1530
1614
|
const facts = factsFrom(resolved);
|
|
1531
1615
|
const unresolved = gaps.gaps.filter((gap) => !gap.resolved);
|
|
1532
1616
|
const settled = gaps.gaps.filter((gap) => gap.resolved && gap.action !== "dismissed");
|
|
1617
|
+
const orphaned = gaps.orphaned_answers ?? [];
|
|
1533
1618
|
return ok({ ...base, ...facts, data: gaps }, [
|
|
1534
1619
|
unresolved.length === 0 ? "Every open question and assumption in this design has been dealt with." : `${unresolved.length} unresolved:`,
|
|
1535
1620
|
...unresolved.map((gap) => `- [${gap.kind}] ${gap.text}`),
|
|
@@ -1538,6 +1623,16 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1538
1623
|
`${settled.length} settled during review \u2014 treat these as decided:`,
|
|
1539
1624
|
...settled.map((gap) => `- [${gap.kind}] ${gap.text} \u2192 ${settledAnswer(gap)}`)
|
|
1540
1625
|
] : [],
|
|
1626
|
+
// A gap is identified by a hash of its own text, so a regeneration that
|
|
1627
|
+
// re-words a question strands the answer that settled it. The server
|
|
1628
|
+
// reports those rather than dropping them, and then this dropped them.
|
|
1629
|
+
// A count and a sentence, not rows: the stored answer has no text to
|
|
1630
|
+
// show, and it is not work anyone can pick up.
|
|
1631
|
+
...orphaned.length ? [
|
|
1632
|
+
"",
|
|
1633
|
+
`${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.`,
|
|
1634
|
+
...orphaned.filter((answer) => answer.adr_id).map((answer) => `- [${answer.kind}] ${answer.action} \u2192 Decision ${answer.adr_id}`)
|
|
1635
|
+
] : [],
|
|
1541
1636
|
"",
|
|
1542
1637
|
"The unresolved ones are questions for a person, not for you to answer. Raise",
|
|
1543
1638
|
"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.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|