@pymodel/niblet 0.3.2 → 0.4.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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/skill/niblet/SKILL.md +12 -12
- package/skill/niblet/references/connection.md +16 -16
- package/src/server.mjs +146 -29
package/README.md
CHANGED
|
@@ -80,9 +80,9 @@ Or use the equivalent host configuration:
|
|
|
80
80
|
}
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
The local adapter's three catalogue tools call REST `/v1`, not hosted MCP.
|
|
83
|
+
The local adapter's three catalogue tools call REST `/v1`, not hosted MCP. Public `niblet_at_…` account keys created at [niblet.com/account](https://www.niblet.com/account) authorize both `/v1` and `/mcp`. Set `NIBLET_TOKEN` to that key, plus `NIBLET_API_ORIGIN` and `NIBLET_MEDIA_ORIGIN` only when targeting a self-hosted deployment. Skill resources, `niblet_help`, and `niblet_status` configuration remain available with no token.
|
|
84
84
|
|
|
85
|
-
**Ownership decision:** keep public, account-key catalogue access in the hosted HTTP MCP. Keep bundled resources, playbook prompts, local diagnostics, and the optional
|
|
85
|
+
**Ownership decision:** keep public, account-key catalogue access in the hosted HTTP MCP. Keep bundled resources, playbook prompts, local diagnostics, and the optional REST bridge in this stdio adapter. Do not proxy hosted MCP through this package or add its local-only surfaces to the hosted service; keep only the three shared catalogue contracts in lockstep.
|
|
86
86
|
|
|
87
87
|
Node.js 24.15+; npx fetches the package on first launch. Keep every token in the host's secret/environment facility, never in a committed file or chat message.
|
|
88
88
|
|
package/package.json
CHANGED
package/skill/niblet/SKILL.md
CHANGED
|
@@ -3,12 +3,12 @@ name: niblet
|
|
|
3
3
|
description: Keep interface work anchored to the product it belongs to instead of a generic template. Sets a short design contract, builds from the components and tokens already in the codebase, covers the states a surface can actually reach, and closes by looking at the rendered result. Use when building, reworking, or assessing a web or native interface. Trigger with "niblet", "niblet skill", "niblet designer ui", or "niblet review". Skip backend, CLI, data, and infrastructure work, prose-only tasks, and questions the product's own design system already settles.
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
|
-
version:
|
|
7
|
-
author:
|
|
8
|
-
organization:
|
|
9
|
-
source:
|
|
10
|
-
compatibility:
|
|
11
|
-
tags:
|
|
6
|
+
version: '0.4.1'
|
|
7
|
+
author: 'Mohamed Elkholy (elkaix)'
|
|
8
|
+
organization: 'PyModel'
|
|
9
|
+
source: 'https://github.com/PyModel/niblet-skill-mcp'
|
|
10
|
+
compatibility: 'Claude Code, Codex, Cursor, and GitHub Copilot, or any agent that can open repository files and call an MCP tool.'
|
|
11
|
+
tags: 'interface-design, product-ui, design-contract, state-coverage, accessibility, native'
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# Niblet
|
|
@@ -31,12 +31,12 @@ Build an interface that belongs to this product, not a generic template. [Niblet
|
|
|
31
31
|
|
|
32
32
|
Choose by the job of each surface, not the repository's category. Different surfaces in one product may use different modes. Persist the choice only in that surface's brief when persistence is requested; do not impose a project-wide mode.
|
|
33
33
|
|
|
34
|
-
| Mode
|
|
35
|
-
|
|
|
36
|
-
| **Persuade**
|
|
37
|
-
| **Operate**
|
|
38
|
-
| **Read**
|
|
39
|
-
| **Experience** | Engage with the work itself | Let the artifact lead; keep navigation and chrome subordinate but usable
|
|
34
|
+
| Mode | User's job | Design priority | Common surfaces |
|
|
35
|
+
| -------------- | --------------------------- | -------------------------------------------------------------------------- | -------------------------------------------- |
|
|
36
|
+
| **Persuade** | Decide and act | Make the proposition, evidence, trade-offs, and next action understandable | Marketing, pricing, acquisition |
|
|
37
|
+
| **Operate** | Complete a task | Legible state, efficient controls, predictable navigation, and recovery | Apps, dashboards, settings, native utilities |
|
|
38
|
+
| **Read** | Understand | Comprehension, typography, reading rhythm, navigation, and useful examples | Documentation, articles, guides |
|
|
39
|
+
| **Experience** | Engage with the work itself | Let the artifact lead; keep navigation and chrome subordinate but usable | Portfolios, galleries, interactive work |
|
|
40
40
|
|
|
41
41
|
Mode is a prioritization tool, not a visual preset. A pricing page still needs usable controls; a gallery still needs accessible navigation; an application can have character without obscuring its tasks.
|
|
42
42
|
|
|
@@ -8,7 +8,7 @@ The skill can work from the repository, product brief, and supplied screenshots
|
|
|
8
8
|
|
|
9
9
|
## Local stdio MCP package
|
|
10
10
|
|
|
11
|
-
Prerequisite: Node.js **24.15 or later**. The adapter's bundled documents, `niblet_help`, and `niblet_status` work without a token. Its three catalogue tools call
|
|
11
|
+
Prerequisite: Node.js **24.15 or later**. The adapter's bundled documents, `niblet_help`, and `niblet_status` work without a token. Its three catalogue tools call REST `/v1`. A public `niblet_at_…` account key created at [niblet.com/account](https://www.niblet.com/account) authorizes both `/v1` and hosted `/mcp`.
|
|
12
12
|
|
|
13
13
|
The shortest local configuration uses the published package:
|
|
14
14
|
|
|
@@ -31,23 +31,23 @@ For a host using the common `mcpServers` JSON configuration shape:
|
|
|
31
31
|
|
|
32
32
|
Adapt the shape to the host's documented configuration. The bundled `mcp.json` contains this token-free template; it does not load `.env`.
|
|
33
33
|
|
|
34
|
-
For catalogue access
|
|
35
|
-
|
|
36
|
-
Public catalogue access should use the hosted MCP endpoint with an account key created at `https://www.niblet.com/account`; do not put that account key in this adapter's `NIBLET_TOKEN`.
|
|
34
|
+
Niblet itself is a hosted service — there is nothing to self-host. For catalogue access, point this adapter at the hosted API: set `NIBLET_TOKEN` to an account key created at `https://www.niblet.com/account` (one key works on the REST catalogue and the MCP endpoint alike), and leave `NIBLET_API_ORIGIN` and `NIBLET_MEDIA_ORIGIN` unset for the hosted defaults. Prefer a host-managed secret/environment facility and keep keys out of commits, screenshots, queries, and chat. From a source checkout, a manual environment-file launch is `node --env-file=/absolute/path/to/private.env src/index.mjs`; plain `npm start` only inherits its process environment.
|
|
37
35
|
|
|
38
36
|
The package contacts `https://api.niblet.com` by default, or the HTTP(S) origin in `NIBLET_API_ORIGIN`. It sends the token as bearer authentication for API requests, and never to the media origin. `NIBLET_TOKEN` configures this local adapter; it is not automatically a remote HTTP client's authentication setting.
|
|
39
37
|
|
|
40
|
-
After the host starts the entry, inspect its observed tool, resource, and prompt inventory. A saved configuration is not proof of a connection. `niblet://skill` and the four `niblet://skill/{commands,connection,evidence,native}` resources return the bundled documents without a token. The local adapter also registers every playbook entry as an MCP prompt named `niblet-<command>`. Catalogue calls require the
|
|
38
|
+
After the host starts the entry, inspect its observed tool, resource, and prompt inventory. A saved configuration is not proof of a connection. `niblet://skill` and the four `niblet://skill/{commands,connection,evidence,native}` resources return the bundled documents without a token. The local adapter also registers every playbook entry as an MCP prompt named `niblet-<command>`. Catalogue calls require a key — against the hosted service, an account key from `/account`.
|
|
39
|
+
|
|
40
|
+
If a catalogue tool fails, read the error: it tells you whether to relay a config change to the user (missing, unexpanded, or placeholder token; website origin instead of `api.niblet.com`) or to continue from `niblet://skill` without retrying. Do not conclude the catalogue is empty from an authentication failure. Hosted MCP at `https://api.niblet.com/mcp` is the other door for the same key; it does not expose `niblet_help` or `niblet_status`.
|
|
41
41
|
|
|
42
42
|
### Tool inputs
|
|
43
43
|
|
|
44
44
|
Tool arguments use **`query`**, not `q`. The adapter translates `query` to the REST API's `q` query parameter. Omitted optional fields use the defaults below.
|
|
45
45
|
|
|
46
|
-
| Tool
|
|
47
|
-
|
|
|
48
|
-
| `find_ui_references`
|
|
49
|
-
| `find_ui_materials`
|
|
50
|
-
| `get_design_reference` | one of `screenId` (from a reference) or `packSlug`
|
|
46
|
+
| Tool | Required arguments | Optional arguments | Returned data |
|
|
47
|
+
| ---------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
48
|
+
| `find_ui_references` | `query`: string, 1–240 characters | `platform`: `ios` or `web`; `limit`: integer 1–3, default 2; `selectedIds`: one to three screen IDs, each 1–160 characters; `clientSkillVersion`: string, 1–64 characters | Human-readable reference lines and images in `content`; typed references in `structuredContent` |
|
|
49
|
+
| `find_ui_materials` | `query`: string, 1–240 characters; `kind`: `font`, `icon`, `animated_icon`, or `pack` | `platform`: `ios` or `web`; `limit`: integer 1–3, default 2; `selectedId`: string, 1–160 characters; `userConfirmed`: `true`; `clientSkillVersion`: string, 1–64 characters | Human-readable materials in `content`; typed name, license, description, and URL records in `structuredContent` |
|
|
50
|
+
| `get_design_reference` | one of `screenId` (from a reference) or `packSlug` | `sections`: one or more of `overview`, `colors`, `typography`, `components`, `provenance`; `clientSkillVersion`: string, 1–64 characters | The requested markdown in `content`; typed slug, name, theme, markdown, and returned section names in `structuredContent` |
|
|
51
51
|
|
|
52
52
|
Without `selectedIds`, `find_ui_references` searches and attaches thumbnail images. With `selectedIds`, it reads those exact screens and attaches inspection-quality images; an ID the catalogue does not hold is skipped rather than failing the call. `query` is required in both cases.
|
|
53
53
|
|
|
@@ -107,11 +107,11 @@ The remote service does not promise the `niblet://skill` resource. Neither deplo
|
|
|
107
107
|
|
|
108
108
|
## Capability checks and manual alternatives
|
|
109
109
|
|
|
110
|
-
| Requested helper | Capability to inspect
|
|
111
|
-
|
|
|
112
|
-
| `doctor`
|
|
113
|
-
| `hooks`
|
|
114
|
-
| `pin` / `unpin`
|
|
115
|
-
| `live`
|
|
110
|
+
| Requested helper | Capability to inspect | Honest alternative |
|
|
111
|
+
| ---------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
112
|
+
| `doctor` | Host connection state, runtime/path configuration, token presence, tool inventory | Explain the observed failed boundary and the needed configuration change; no bundled diagnostic executable |
|
|
113
|
+
| `hooks` | Host-specific hook API and existing event configuration | Invoke the finish gate manually before handoff; no bundled hook installer |
|
|
114
|
+
| `pin` / `unpin` | Host's documented command-shortcut registration | Invoke “Niblet <command> <target>” directly; no bundled shortcut installer |
|
|
115
|
+
| `live` | Authorized browser/simulator session and available interaction tools | Work from supplied screenshots and targeted manual inspection; no bundled browser service or watcher |
|
|
116
116
|
|
|
117
117
|
The full task instructions for these helpers are in [the command playbook](commands.md). Confirm capabilities through real host results, not inferred availability from a command name.
|
package/src/server.mjs
CHANGED
|
@@ -51,18 +51,23 @@ function textResult(text, structuredContent) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
* A 401 is nearly always a token mismatch on the user's machine, so the
|
|
55
|
-
* says exactly what to check and asks the agent to relay it. The token
|
|
56
|
-
* never shown; its length is enough to tell two credentials apart.
|
|
54
|
+
* A 401 is nearly always a token or origin mismatch on the user's machine, so the
|
|
55
|
+
* message says exactly what to check and asks the agent to relay it. The token
|
|
56
|
+
* itself is never shown; its length is enough to tell two credentials apart.
|
|
57
57
|
*/
|
|
58
|
+
const LOCAL_CONTINUE =
|
|
59
|
+
'Do not retry catalogue tools. Do not conclude the catalogue is empty. niblet_help and niblet://skill remain available without a key.';
|
|
60
|
+
|
|
58
61
|
function authenticationFailed(token, apiOrigin) {
|
|
59
62
|
const length = typeof token === 'string' ? token.trim().length : 0;
|
|
60
63
|
return [
|
|
61
64
|
`Niblet API authentication failed (HTTP 401): ${apiOrigin} rejected the configured NIBLET_TOKEN (${length} characters, not shown).`,
|
|
62
|
-
'Tell the user: the token this MCP server is running with is not one the API accepts.',
|
|
65
|
+
'Tell the user: the token this MCP server is running with is not one the API at that origin accepts.',
|
|
63
66
|
'Most often a NIBLET_TOKEN exported in the shell (e.g. ~/.zshrc, ~/.zshrc.local) overrides the one in the MCP .env file, because node --env-file never replaces a variable that is already set.',
|
|
64
|
-
'
|
|
67
|
+
'Another cause is pointing this adapter at the wrong origin, or using a key from a different deployment.',
|
|
68
|
+
`To fix: create a key at https://www.niblet.com/account, set it as NIBLET_TOKEN (or connect the host to https://api.niblet.com/mcp with Authorization: Bearer niblet_at_…), make the shell export and the .env file agree (or remove the export), then restart the MCP server so it re-reads its environment.`,
|
|
65
69
|
'Run niblet_status to confirm the fix.',
|
|
70
|
+
LOCAL_CONTINUE,
|
|
66
71
|
].join(' ');
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -78,7 +83,14 @@ function retryAfterSeconds(value) {
|
|
|
78
83
|
function httpError(status, context = {}) {
|
|
79
84
|
if (status >= 300 && status < 400) return 'Niblet API redirects are not allowed.';
|
|
80
85
|
if (status === 401) return authenticationFailed(context.token, context.apiOrigin);
|
|
81
|
-
if (status === 403)
|
|
86
|
+
if (status === 403) {
|
|
87
|
+
return [
|
|
88
|
+
'Niblet API access denied (HTTP 403).',
|
|
89
|
+
'Tell the user: this origin refused the request. That is often a WAF or a key that is not allowed on this path, not a missing catalogue.',
|
|
90
|
+
'To fix: use a niblet_at_ account key from https://www.niblet.com/account against https://api.niblet.com. Do not rotate the key unless the API said it was unrecognised.',
|
|
91
|
+
LOCAL_CONTINUE,
|
|
92
|
+
].join(' ');
|
|
93
|
+
}
|
|
82
94
|
if (status === 404) return 'The requested Niblet resource was not found (HTTP 404).';
|
|
83
95
|
if (status === 429) {
|
|
84
96
|
const seconds = retryAfterSeconds(context.retryAfter);
|
|
@@ -88,6 +100,74 @@ function httpError(status, context = {}) {
|
|
|
88
100
|
return `Niblet API request failed (HTTP ${status}).`;
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
function originNote(apiOrigin) {
|
|
104
|
+
let host;
|
|
105
|
+
try {
|
|
106
|
+
host = new URL(apiOrigin).hostname;
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (host === 'api.niblet.com' || host === 'localhost' || host === '127.0.0.1') return null;
|
|
111
|
+
if (wrongOriginMessage(apiOrigin)) return null;
|
|
112
|
+
return `Niblet API origin is ${host}, not api.niblet.com. Tell the user: confirm this is their own deployment. Catalogue calls will use this origin.`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function leaksCredential(text, token) {
|
|
116
|
+
if (/<|&#/i.test(text)) return true;
|
|
117
|
+
if (typeof token !== 'string' || token.trim() === '') return false;
|
|
118
|
+
const value = token.trim();
|
|
119
|
+
if (text.includes(value)) return true;
|
|
120
|
+
if (value.length >= 24 && text.includes(value.slice(8))) return true;
|
|
121
|
+
if (text.includes(Buffer.from(value).toString('base64'))) return true;
|
|
122
|
+
if (text.includes(encodeURIComponent(value))) return true;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function messageFromErrorResponse(response, fallback, token) {
|
|
127
|
+
try {
|
|
128
|
+
const bytes = await readBounded(response);
|
|
129
|
+
const data = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
130
|
+
const body = data && typeof data === 'object' && typeof data.error === 'string' ? data.error.trim() : '';
|
|
131
|
+
if (!body || leaksCredential(body, token)) return fallback;
|
|
132
|
+
if (body.startsWith('Niblet API')) return body;
|
|
133
|
+
if (fallback.startsWith('Niblet API authentication failed')) {
|
|
134
|
+
return `Niblet API authentication failed (HTTP 401). ${body}`;
|
|
135
|
+
}
|
|
136
|
+
if (fallback.startsWith('Niblet API access denied')) {
|
|
137
|
+
return `Niblet API access denied (HTTP 403). ${body}`;
|
|
138
|
+
}
|
|
139
|
+
return body;
|
|
140
|
+
} catch {
|
|
141
|
+
return fallback;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function wrongOriginMessage(apiOrigin) {
|
|
146
|
+
let host;
|
|
147
|
+
try {
|
|
148
|
+
host = new URL(apiOrigin).hostname;
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
if (host === 'niblet.com' || host === 'www.niblet.com') {
|
|
153
|
+
return [
|
|
154
|
+
`Niblet API origin is the public website (${host}), not the API.`,
|
|
155
|
+
'Tell the user: this MCP is pointed at niblet.com instead of api.niblet.com.',
|
|
156
|
+
'To fix: leave NIBLET_API_ORIGIN unset or set it to https://api.niblet.com, then restart this MCP server.',
|
|
157
|
+
LOCAL_CONTINUE,
|
|
158
|
+
].join(' ');
|
|
159
|
+
}
|
|
160
|
+
if (host === 'media.niblet.com') {
|
|
161
|
+
return [
|
|
162
|
+
'Niblet API origin is the media host, not the API.',
|
|
163
|
+
'Tell the user: NIBLET_API_ORIGIN is set to the media origin.',
|
|
164
|
+
'To fix: set NIBLET_API_ORIGIN to https://api.niblet.com or unset it, then restart this MCP server.',
|
|
165
|
+
LOCAL_CONTINUE,
|
|
166
|
+
].join(' ');
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
91
171
|
/** Read a bounded body into one buffer; the caller decides how to decode it. */
|
|
92
172
|
async function readBounded(response) {
|
|
93
173
|
const declaredLength = response.headers.get('content-length');
|
|
@@ -275,14 +355,44 @@ export function createServer({
|
|
|
275
355
|
|
|
276
356
|
function credentialError() {
|
|
277
357
|
if (typeof token !== 'string' || token.trim() === '') {
|
|
278
|
-
return
|
|
358
|
+
return [
|
|
359
|
+
'NIBLET_TOKEN is required for Niblet catalogue tools.',
|
|
360
|
+
'Tell the user: this local MCP has no key, so search cannot run.',
|
|
361
|
+
'To fix: create a key at https://www.niblet.com/account, set NIBLET_TOKEN in the MCP server environment, then restart this server; or connect the host to https://api.niblet.com/mcp with Authorization: Bearer niblet_at_….',
|
|
362
|
+
LOCAL_CONTINUE,
|
|
363
|
+
].join(' ');
|
|
364
|
+
}
|
|
365
|
+
if (/^\$\{?[A-Z0-9_]+\}?$/.test(token)) {
|
|
366
|
+
return [
|
|
367
|
+
'NIBLET_TOKEN is the literal environment-variable name, not a key.',
|
|
368
|
+
'Tell the user: the MCP config stored the variable name unexpanded.',
|
|
369
|
+
'To fix: put the key itself (it starts with niblet_at_) in the MCP server environment, then restart.',
|
|
370
|
+
LOCAL_CONTINUE,
|
|
371
|
+
].join(' ');
|
|
372
|
+
}
|
|
373
|
+
if (token === 'YOUR_NIBLET_KEY' || /^(<|\[)?your[-_ ]?niblet[-_ ]?(key|token)(>|\])?$/i.test(token)) {
|
|
374
|
+
return [
|
|
375
|
+
'NIBLET_TOKEN is the setup placeholder, not a key.',
|
|
376
|
+
'Tell the user: they still have the example text in the MCP config.',
|
|
377
|
+
'To fix: create a key at https://www.niblet.com/account, put it in the MCP server environment, then restart.',
|
|
378
|
+
LOCAL_CONTINUE,
|
|
379
|
+
].join(' ');
|
|
279
380
|
}
|
|
280
381
|
if (!/^[A-Za-z0-9._~+/-]+=*$/.test(token)) {
|
|
281
|
-
return
|
|
382
|
+
return [
|
|
383
|
+
'NIBLET_TOKEN is not a valid bearer token.',
|
|
384
|
+
'Tell the user: the configured value is not a usable key.',
|
|
385
|
+
'To fix: paste a niblet_at_ key from https://www.niblet.com/account into the MCP server environment, then restart.',
|
|
386
|
+
LOCAL_CONTINUE,
|
|
387
|
+
].join(' ');
|
|
282
388
|
}
|
|
283
389
|
return null;
|
|
284
390
|
}
|
|
285
391
|
|
|
392
|
+
function connectionError() {
|
|
393
|
+
return wrongOriginMessage(API_ORIGIN) || credentialError();
|
|
394
|
+
}
|
|
395
|
+
|
|
286
396
|
/** One bounded, fixed-origin, authenticated GET. Resolves to {ok:true,data} or {ok:false,message,status}. */
|
|
287
397
|
async function requestJson(segments, params, callerSignal) {
|
|
288
398
|
if (callerSignal?.aborted) return { ok: false, message: 'Niblet API request was cancelled.' };
|
|
@@ -317,15 +427,20 @@ export function createServer({
|
|
|
317
427
|
signal.throwIfAborted();
|
|
318
428
|
if (response.redirected) throw new ApiError('Niblet API redirects are not allowed.');
|
|
319
429
|
if (!response.ok) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
430
|
+
const retryAfter = response.headers.get('retry-after');
|
|
431
|
+
const fallback = httpError(response.status, {
|
|
432
|
+
token,
|
|
433
|
+
apiOrigin: API_ORIGIN,
|
|
434
|
+
retryAfter,
|
|
435
|
+
});
|
|
436
|
+
let message = await messageFromErrorResponse(response, fallback, token);
|
|
437
|
+
if (response.status === 429) {
|
|
438
|
+
const seconds = retryAfterSeconds(retryAfter);
|
|
439
|
+
if (seconds !== null && !/Retry after/i.test(message)) {
|
|
440
|
+
message = `${message} Retry after ${seconds} seconds.`;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return { ok: false, message, status: response.status };
|
|
329
444
|
}
|
|
330
445
|
const data = await readJson(response);
|
|
331
446
|
signal.throwIfAborted();
|
|
@@ -393,8 +508,8 @@ export function createServer({
|
|
|
393
508
|
outputSchema: FindUiReferencesOutputSchema,
|
|
394
509
|
annotations,
|
|
395
510
|
}, async (input, extra) => {
|
|
396
|
-
const
|
|
397
|
-
if (
|
|
511
|
+
const connected = connectionError();
|
|
512
|
+
if (connected) return errorResult(connected);
|
|
398
513
|
|
|
399
514
|
if (input.selectedIds?.length) {
|
|
400
515
|
// Missing ids are omitted, and the remaining screens are numbered contiguously, as the catalogue does.
|
|
@@ -484,8 +599,8 @@ export function createServer({
|
|
|
484
599
|
{ materials: [], kind: input.kind },
|
|
485
600
|
);
|
|
486
601
|
}
|
|
487
|
-
const
|
|
488
|
-
if (
|
|
602
|
+
const connected = connectionError();
|
|
603
|
+
if (connected) return errorResult(connected);
|
|
489
604
|
|
|
490
605
|
const result = await requestJson(['materials'], {
|
|
491
606
|
q: input.query,
|
|
@@ -538,8 +653,8 @@ export function createServer({
|
|
|
538
653
|
outputSchema: GetDesignReferenceOutputSchema,
|
|
539
654
|
annotations,
|
|
540
655
|
}, async (input, extra) => {
|
|
541
|
-
const
|
|
542
|
-
if (
|
|
656
|
+
const connected = connectionError();
|
|
657
|
+
if (connected) return errorResult(connected);
|
|
543
658
|
|
|
544
659
|
const result = await requestJson(['design-reference'], {
|
|
545
660
|
screenId: input.screenId,
|
|
@@ -632,7 +747,7 @@ export function createServer({
|
|
|
632
747
|
}
|
|
633
748
|
lines.push('', 'Reference documents (read as MCP resources):');
|
|
634
749
|
for (const [slug, doc] of Object.entries(SKILL_DOCS)) lines.push(` ${uriFor(slug)} — ${doc.title}`);
|
|
635
|
-
lines.push('', 'Catalogue tools: find_ui_references (real full-screen references), find_ui_materials (license-recorded fonts and icons), get_design_reference (the recorded colors, typography, and components behind a web screen). All three need NIBLET_TOKEN
|
|
750
|
+
lines.push('', 'Catalogue tools: find_ui_references (real full-screen references), find_ui_materials (license-recorded fonts and icons), get_design_reference (the recorded colors, typography, and components behind a web screen). All three need a niblet_at_ key as NIBLET_TOKEN, or connect the host to https://api.niblet.com/mcp with that key. Run niblet_status to check. Reference retrieval is optional and never a prerequisite to useful work.');
|
|
636
751
|
lines.push('With no target or command, present this menu and wait for a choice rather than making changes.');
|
|
637
752
|
return textResult(lines.join('\n'));
|
|
638
753
|
});
|
|
@@ -650,9 +765,12 @@ export function createServer({
|
|
|
650
765
|
|
|
651
766
|
// Presence and shape only — the playbook's doctor entry requires never displaying it.
|
|
652
767
|
const credential = credentialError();
|
|
768
|
+
const originWrong = wrongOriginMessage(API_ORIGIN);
|
|
653
769
|
if (typeof token !== 'string' || token.trim() === '') lines.push('Token: not configured. Set NIBLET_TOKEN in the MCP server environment.');
|
|
654
770
|
else if (credential) lines.push('Token: present but malformed for a bearer credential. Check NIBLET_TOKEN.');
|
|
655
771
|
else lines.push(`Token: present (${token.trim().length} characters, not shown).`);
|
|
772
|
+
const note = originNote(API_ORIGIN);
|
|
773
|
+
if (note) lines.push(`Origin note: ${note}`);
|
|
656
774
|
|
|
657
775
|
const docs = await Promise.all(Object.keys(SKILL_DOCS).map(async (slug) => {
|
|
658
776
|
try {
|
|
@@ -670,18 +788,17 @@ export function createServer({
|
|
|
670
788
|
lines.push('', 'API not contacted (probe disabled).');
|
|
671
789
|
return textResult(lines.join('\n'));
|
|
672
790
|
}
|
|
673
|
-
if (credential) {
|
|
674
|
-
lines.push('',
|
|
791
|
+
if (originWrong || credential) {
|
|
792
|
+
lines.push('', originWrong || credential);
|
|
675
793
|
return textResult(lines.join('\n'));
|
|
676
794
|
}
|
|
677
795
|
|
|
678
796
|
const result = await requestJson(['stats'], {}, extra.signal);
|
|
679
797
|
if (!result.ok) {
|
|
680
798
|
// Any HTTP status means the origin answered, which is what reachability asks.
|
|
681
|
-
//
|
|
682
|
-
// from a healthy deployment, not a failure.
|
|
799
|
+
// A 404 still means the origin is up; counts come from a current /v1/stats.
|
|
683
800
|
if (result.status === 404) {
|
|
684
|
-
lines.push('', 'API check: reachable — the configured origin answered
|
|
801
|
+
lines.push('', 'API check: reachable — the configured origin answered, but /v1/stats was not there. Tell the user: NIBLET_API_ORIGIN may point at the website or an old deployment, not https://api.niblet.com. To fix: unset NIBLET_API_ORIGIN for hosted, then restart this MCP server.');
|
|
685
802
|
return textResult(lines.join('\n'));
|
|
686
803
|
}
|
|
687
804
|
if (result.status !== undefined) {
|