hackshop-mcp 0.0.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/CONTRIBUTING.md +68 -0
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/SAMPLING.md +76 -0
- package/catalog.json +97 -0
- package/dist/catalog/load.d.ts +7 -0
- package/dist/catalog/load.js +73 -0
- package/dist/catalog/load.js.map +1 -0
- package/dist/catalog/schema.d.ts +83 -0
- package/dist/catalog/schema.js +41 -0
- package/dist/catalog/schema.js.map +1 -0
- package/dist/safety.d.ts +7 -0
- package/dist/safety.js +31 -0
- package/dist/safety.js.map +1 -0
- package/dist/sampling.d.ts +8 -0
- package/dist/sampling.js +65 -0
- package/dist/sampling.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +99 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/assess_hackability.d.ts +29 -0
- package/dist/tools/assess_hackability.js +43 -0
- package/dist/tools/assess_hackability.js.map +1 -0
- package/dist/tools/propose_hardware.d.ts +38 -0
- package/dist/tools/propose_hardware.js +117 -0
- package/dist/tools/propose_hardware.js.map +1 -0
- package/examples/01-eo-revival.json +7 -0
- package/examples/02-house-calendar.json +9 -0
- package/examples/TEMPLATE.json.example +8 -0
- package/package.json +61 -0
- package/tags.md +39 -0
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Contributing to hackshop-mcp
|
|
2
|
+
|
|
3
|
+
The catalog is the moat. New devices come in via PR. Tags come in via `tags.md` edits. Provenance matters.
|
|
4
|
+
|
|
5
|
+
## Adding a device
|
|
6
|
+
|
|
7
|
+
1. **Pick an id.** lowercase-kebab-case, unique. Format: `manufacturer-model-revision`. Example: `electric-objects-eo1`.
|
|
8
|
+
2. **Verify hackability.** Either:
|
|
9
|
+
- You've personally flashed / modded this device → `brick_provenance: "founder-verified"` (only the maintainer's commits count for this).
|
|
10
|
+
- You've found credible community reports (3+ unique sources, not all linking back to the same forum thread) → `brick_provenance: "community-reported"`.
|
|
11
|
+
- Otherwise → `brick_provenance: "llm-inferred"`. Note: for `category: "handheld" | "sbc"`, llm-inferred brick-risk is **stripped from output** by the safety rule. The device will still appear, but with "brick-risk unknown."
|
|
12
|
+
3. **Write the entry** in `catalog.json`. All fields required (see `src/catalog/schema.ts`).
|
|
13
|
+
4. **Tags must be in `tags.md`.** Server fails to start otherwise. If you need a new tag, edit `tags.md` in the same PR.
|
|
14
|
+
5. **Notes capped at 500 chars.** Keep them concrete. If the same thing keeps showing up across many devices, it should become a structured field, not a notes blurb.
|
|
15
|
+
6. **`last_verified` is the date you confirmed the entry, in `YYYY-MM-DD` format.** Quarterly re-verification is the eventual goal.
|
|
16
|
+
|
|
17
|
+
## Adding a tag
|
|
18
|
+
|
|
19
|
+
1. Edit `tags.md`. Format: `- \`tag-name\` — short description`.
|
|
20
|
+
2. Tag names: lowercase-kebab-case. Singular. No emoji.
|
|
21
|
+
3. Try to fit into an existing section (Output / Audio / Input / etc.) before creating a new one.
|
|
22
|
+
4. Total tag count target: ~25. If you're pushing past 30, the vocabulary is too granular — fold tags together.
|
|
23
|
+
|
|
24
|
+
## PR template
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
## Device added
|
|
28
|
+
- id: ...
|
|
29
|
+
- category: ...
|
|
30
|
+
- provenance: founder-verified | community-reported | llm-inferred
|
|
31
|
+
|
|
32
|
+
## Verification (required for non-llm-inferred)
|
|
33
|
+
- [ ] I have flashed / modded this device personally, OR
|
|
34
|
+
- [ ] I have linked 3+ credible community sources
|
|
35
|
+
|
|
36
|
+
## Sources
|
|
37
|
+
- (url)
|
|
38
|
+
- (url)
|
|
39
|
+
|
|
40
|
+
## Tag changes (if any)
|
|
41
|
+
- New tag: ...
|
|
42
|
+
- Reason: ...
|
|
43
|
+
|
|
44
|
+
## Brick-risk provenance check (for handheld | sbc)
|
|
45
|
+
- [ ] I understand that llm-inferred provenance for handheld/sbc results in brick-risk being stripped from output (the safety rule).
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## What not to add
|
|
49
|
+
|
|
50
|
+
- Devices that aren't hackable. The catalog is for hackable hardware only.
|
|
51
|
+
- Devices you can't find in any secondary market. The point is helping users acquire them.
|
|
52
|
+
- Devices with brand-new firmware updates that closed the hack. (These get removed quarterly.)
|
|
53
|
+
|
|
54
|
+
## Tests must pass
|
|
55
|
+
|
|
56
|
+
Before opening a PR:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm run validate # catalog + tags
|
|
60
|
+
npm test # safety, schema, lookup
|
|
61
|
+
npm run regress # regression examples
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Boot validation will catch tag drift; the regression runner will catch catalog/prompt drift on the existing examples.
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
By contributing, you agree your contribution is MIT-licensed.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Miguel Sanchez-Grice
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# hackshop-mcp
|
|
2
|
+
|
|
3
|
+
Hardware-literate AI scout for tinkerers. Idea-to-hardware mapping via MCP.
|
|
4
|
+
|
|
5
|
+
You describe a project. The agent surfaces 3-5 hackable hardware options you wouldn't have thought of, with brick risk, firmware links, and a suggested eBay search query. Compose with [`ebay-mcp`](https://github.com/YosefHayim/ebay-mcp) for live listings.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
A tinkerer has an idea. The idea would be cooler with the right piece of hardware attached: an old screen, an abandoned smart speaker, a bricked frame, a hackable handheld. The tinkerer doesn't know what hardware exists, what's hackable, or what would creatively *fit* the idea. So the idea stays purely software, or gets paired with a Raspberry Pi.
|
|
10
|
+
|
|
11
|
+
This is a hardware-knowledge layer on top of LLMs. Two tools, fifty hand-vetted devices, one closed-set tag vocabulary, and a brick-risk safety rule that won't let the agent fabricate a score for hardware classes where bricks are unrecoverable.
|
|
12
|
+
|
|
13
|
+
## Status
|
|
14
|
+
|
|
15
|
+
V0.0.1 — local development only. Not yet on npm or the MCP registry.
|
|
16
|
+
|
|
17
|
+
## Tools
|
|
18
|
+
|
|
19
|
+
### `propose_hardware(idea, budget_usd?, constraints?)`
|
|
20
|
+
|
|
21
|
+
Returns 3-5 hardware proposals, each with:
|
|
22
|
+
|
|
23
|
+
- `name` and `category`
|
|
24
|
+
- `why_this_fits` — one sentence referencing your idea explicitly
|
|
25
|
+
- `hack_difficulty` (1-5)
|
|
26
|
+
- `brick_risk` — numeric score, OR null + "unknown" label for hard-to-recover categories with LLM-inferred risk
|
|
27
|
+
- `brick_risk_disclaimer` — present when llm-inferred but score retained
|
|
28
|
+
- `firmware_links` — github repos, hackaday articles
|
|
29
|
+
- `community_size` — `tiny | small | active | thriving`
|
|
30
|
+
- `ebay_query_suggestion` — pass to `ebay-mcp`'s search tool for live listings
|
|
31
|
+
|
|
32
|
+
### `assess_hackability(device_name)`
|
|
33
|
+
|
|
34
|
+
Lookup by id, exact name, or substring. Returns the same shape as a single proposal. Use when you have a device in mind and want to verify hackability before searching for one to buy.
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
- TypeScript + `@modelcontextprotocol/sdk`
|
|
39
|
+
- LLM reasoning delegated to the host via `sampling/createMessage` (no Anthropic SDK bundled, no BYO key)
|
|
40
|
+
- Catalog stored as `catalog.json` in the repo (JSON, version-controllable, ~50 devices in V1)
|
|
41
|
+
- Tag vocabulary in `tags.md`, validated at boot — server refuses to start on tag drift
|
|
42
|
+
- eBay integration is **not** in this server. Compose with [`ebay-mcp`](https://github.com/YosefHayim/ebay-mcp) at the host level.
|
|
43
|
+
|
|
44
|
+
## Install (local dev)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git clone <this-repo>
|
|
48
|
+
cd hackshop-mcp
|
|
49
|
+
npm install
|
|
50
|
+
npm run validate # verifies catalog + tags
|
|
51
|
+
npm test # safety + schema + lookup tests
|
|
52
|
+
npm run build # tsc -> dist/
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Day-0 Smoke Test (REQUIRED before scaffolding more)
|
|
56
|
+
|
|
57
|
+
This server depends on `sampling/createMessage`. Some MCP hosts don't support it. Verify yours does first.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Add this to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"hackshop-smoke": {
|
|
69
|
+
"command": "tsx",
|
|
70
|
+
"args": ["/Users/YOU/hackshop-mcp/scripts/smoke.ts"]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Restart Claude Desktop. Ask Claude to call the `smoke_check` tool. If it returns "Smoke OK," your architecture works. If it fails, stop here — `hackshop-mcp` won't work in this host.
|
|
77
|
+
|
|
78
|
+
## Install (target host)
|
|
79
|
+
|
|
80
|
+
After dev is done and smoke passes, add to your MCP client config:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"hackshop": {
|
|
86
|
+
"command": "node",
|
|
87
|
+
"args": ["/Users/YOU/hackshop-mcp/dist/server.js"]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
For idea-to-hardware-to-listings flow, also install `ebay-mcp` from `YosefHayim/ebay-mcp`.
|
|
94
|
+
|
|
95
|
+
## The Story
|
|
96
|
+
|
|
97
|
+
The founder had an Electric Objects EO1 picture frame. The company shut down; the device bricked. He revived it with Claude, ~6 hours of firmware reverse-engineering. Wondered: "what if an agent already knew this stuff?" That's `hackshop-mcp`.
|
|
98
|
+
|
|
99
|
+
## Safety Rule (P0)
|
|
100
|
+
|
|
101
|
+
Bricking unrecoverable hardware is the single failure mode that ends this product. The catalog tracks brick-risk provenance: `founder-verified | community-reported | llm-inferred`. For categories where bricks are unrecoverable (`handheld`, `sbc`), the server **refuses to surface LLM-inferred brick-risk scores**. It returns "brick-risk unknown — research before flashing" instead. This is a tested release gate. See `src/safety.ts` and `test/safety.test.ts`.
|
|
102
|
+
|
|
103
|
+
## Contributing
|
|
104
|
+
|
|
105
|
+
See `CONTRIBUTING.md`. New devices come in via PR; tag changes require a `tags.md` edit; `community-reported` is the default provenance for community contributions.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT.
|
package/SAMPLING.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Sampling Contract
|
|
2
|
+
|
|
3
|
+
This server uses MCP's `sampling/createMessage` to delegate LLM reasoning to the host. No Anthropic / OpenAI SDK is bundled. The host pays for tokens; users don't ship a second API key.
|
|
4
|
+
|
|
5
|
+
## Boot probe
|
|
6
|
+
|
|
7
|
+
`src/server.ts` calls `probeSamplingSupport(server)` after connect. This sends a no-op `sampling/createMessage` and logs whether the host responded. Failure is non-fatal — `propose_hardware` will return degraded responses (raw catalog matches without reasoning).
|
|
8
|
+
|
|
9
|
+
Hosts known to support sampling:
|
|
10
|
+
- Claude Desktop
|
|
11
|
+
- Claude Code
|
|
12
|
+
|
|
13
|
+
Hosts unverified as of 2026-Q1:
|
|
14
|
+
- Cursor
|
|
15
|
+
- other clients
|
|
16
|
+
|
|
17
|
+
## propose_hardware sampling call
|
|
18
|
+
|
|
19
|
+
`propose_hardware` calls `sampleJson<SamplerPick>(...)` once per invocation. Schema:
|
|
20
|
+
|
|
21
|
+
### Inputs
|
|
22
|
+
|
|
23
|
+
System prompt is fixed (see `src/tools/propose_hardware.ts:SYSTEM_PROMPT`).
|
|
24
|
+
|
|
25
|
+
User prompt format:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Project idea: <idea>
|
|
29
|
+
Budget (USD): <budget_usd, if present>
|
|
30
|
+
Constraints: <constraints, if present>
|
|
31
|
+
|
|
32
|
+
Candidate hardware:
|
|
33
|
+
- id: <device-id> | name: <name> | category: <cat> | tags: <a, b, c> | notes: <notes>
|
|
34
|
+
- ...
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Expected output
|
|
38
|
+
|
|
39
|
+
JSON only, no markdown. Code-fence allowed (the parser strips it).
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"picks": [
|
|
44
|
+
{ "id": "device-id-from-candidate-list", "why_this_fits": "one sentence" }
|
|
45
|
+
],
|
|
46
|
+
"rationale": "one paragraph on the overall theme"
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Constraints on the host model
|
|
51
|
+
|
|
52
|
+
- `picks` must contain 0-5 entries.
|
|
53
|
+
- Each `id` MUST be in the candidate list. Hallucinated ids are dropped on the server side, not surfaced to the user.
|
|
54
|
+
- `why_this_fits` must reference the user's idea. The server doesn't enforce this, but the regression suite does.
|
|
55
|
+
|
|
56
|
+
### Token budget
|
|
57
|
+
|
|
58
|
+
`maxTokens: 1500` per call. With ~50 candidates × ~80 chars/line ≈ 4k token input + 1.5k output. Stay below 6k total.
|
|
59
|
+
|
|
60
|
+
### Error handling
|
|
61
|
+
|
|
62
|
+
- Sampling rejects → `sampleJson` returns `null`.
|
|
63
|
+
- Sampling response is non-text → `null`.
|
|
64
|
+
- Sampling response is text but not JSON-parseable → `null`.
|
|
65
|
+
- On `null`, `propose_hardware` retries once. On second `null`, returns degraded response.
|
|
66
|
+
|
|
67
|
+
The retry is in `sampleJson` itself (`src/sampling.ts`).
|
|
68
|
+
|
|
69
|
+
### What the server NEVER does
|
|
70
|
+
|
|
71
|
+
- Bundle an Anthropic / OpenAI / other-provider SDK.
|
|
72
|
+
- Read a user-side `ANTHROPIC_API_KEY` env var.
|
|
73
|
+
- Make HTTP calls to provider endpoints directly.
|
|
74
|
+
- Crash on sampling failure.
|
|
75
|
+
|
|
76
|
+
If a future feature seems to need a direct provider call, treat that as a redesign signal — not a quick patch.
|
package/catalog.json
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "electric-objects-eo1",
|
|
4
|
+
"name": "Electric Objects EO1",
|
|
5
|
+
"category": "display",
|
|
6
|
+
"idea_fit_tags": ["display", "framed-art", "ambient", "creative-coding"],
|
|
7
|
+
"hack_difficulty": 3,
|
|
8
|
+
"brick_risk": 2,
|
|
9
|
+
"brick_provenance": "founder-verified",
|
|
10
|
+
"last_verified": "2026-05-04",
|
|
11
|
+
"firmware_links": [
|
|
12
|
+
"https://github.com/dasl-/electric-objects-revival",
|
|
13
|
+
"https://hackaday.com/tag/electric-objects/"
|
|
14
|
+
],
|
|
15
|
+
"community_size_bucket": "small",
|
|
16
|
+
"notes": "23-inch wall-mount picture frame, abandoned by manufacturer. Linux-based, SSH access via stock firmware bug. Founder revived one in ~6h with Claude assistance."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "raspberry-pi-4b",
|
|
20
|
+
"name": "Raspberry Pi 4 Model B",
|
|
21
|
+
"category": "sbc",
|
|
22
|
+
"idea_fit_tags": ["sbc", "low-power", "creative-coding", "kiosk"],
|
|
23
|
+
"hack_difficulty": 1,
|
|
24
|
+
"brick_risk": 1,
|
|
25
|
+
"brick_provenance": "founder-verified",
|
|
26
|
+
"last_verified": "2026-05-04",
|
|
27
|
+
"firmware_links": [
|
|
28
|
+
"https://www.raspberrypi.com/documentation/"
|
|
29
|
+
],
|
|
30
|
+
"community_size_bucket": "thriving",
|
|
31
|
+
"notes": "Default tinkerer choice. Boring but always works. Use as baseline; recommend more interesting hardware first."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "tidbyt-gen1",
|
|
35
|
+
"name": "Tidbyt (Gen 1)",
|
|
36
|
+
"category": "display",
|
|
37
|
+
"idea_fit_tags": ["display", "ambient", "framed-art", "creative-coding", "low-power"],
|
|
38
|
+
"hack_difficulty": 2,
|
|
39
|
+
"brick_risk": 2,
|
|
40
|
+
"brick_provenance": "community-reported",
|
|
41
|
+
"last_verified": "2026-05-04",
|
|
42
|
+
"firmware_links": [
|
|
43
|
+
"https://github.com/tidbyt/pixlet",
|
|
44
|
+
"https://github.com/tronbyt/server"
|
|
45
|
+
],
|
|
46
|
+
"community_size_bucket": "active",
|
|
47
|
+
"notes": "64x32 LED matrix, ESP32-based. Community firmware (Tronbyt) supports self-hosted apps without Tidbyt cloud."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "barnes-noble-nook-touch",
|
|
51
|
+
"name": "Barnes & Noble Nook Simple Touch (BNRV300)",
|
|
52
|
+
"category": "display",
|
|
53
|
+
"idea_fit_tags": ["display", "e-ink", "low-power", "framed-art", "ambient"],
|
|
54
|
+
"hack_difficulty": 2,
|
|
55
|
+
"brick_risk": 2,
|
|
56
|
+
"brick_provenance": "community-reported",
|
|
57
|
+
"last_verified": "2026-05-04",
|
|
58
|
+
"firmware_links": [
|
|
59
|
+
"https://wiki.mobileread.com/wiki/Nook_ST",
|
|
60
|
+
"https://forum.xda-developers.com/c/barnes-noble-nook.5045/"
|
|
61
|
+
],
|
|
62
|
+
"community_size_bucket": "active",
|
|
63
|
+
"notes": "2011 e-reader, runs Android 2.1 underneath. Well-documented root path via SD card image. Community projects use it as a wall-mounted dashboard / status display."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "amazon-kindle-paperwhite-gen2",
|
|
67
|
+
"name": "Amazon Kindle Paperwhite (Gen 2, 2013)",
|
|
68
|
+
"category": "display",
|
|
69
|
+
"idea_fit_tags": ["display", "e-ink", "low-power", "framed-art", "ambient", "kiosk"],
|
|
70
|
+
"hack_difficulty": 3,
|
|
71
|
+
"brick_risk": 3,
|
|
72
|
+
"brick_provenance": "community-reported",
|
|
73
|
+
"last_verified": "2026-05-04",
|
|
74
|
+
"firmware_links": [
|
|
75
|
+
"https://www.mobileread.com/forums/forumdisplay.php?f=150",
|
|
76
|
+
"https://github.com/koreader/koreader"
|
|
77
|
+
],
|
|
78
|
+
"community_size_bucket": "thriving",
|
|
79
|
+
"notes": "Older Kindles (PW2 era) jailbreak via known firmware paths; newer ones harder. Used widely as low-power dashboard / calendar / weather displays. KOReader runs custom apps on jailbroken units."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "ipad-2",
|
|
83
|
+
"name": "Apple iPad 2",
|
|
84
|
+
"category": "display",
|
|
85
|
+
"idea_fit_tags": ["display", "kiosk", "creative-coding", "framed-art"],
|
|
86
|
+
"hack_difficulty": 2,
|
|
87
|
+
"brick_risk": 1,
|
|
88
|
+
"brick_provenance": "community-reported",
|
|
89
|
+
"last_verified": "2026-05-04",
|
|
90
|
+
"firmware_links": [
|
|
91
|
+
"https://support.apple.com/guided-access",
|
|
92
|
+
"https://github.com/sponsorpay/iPad-Kiosk"
|
|
93
|
+
],
|
|
94
|
+
"community_size_bucket": "thriving",
|
|
95
|
+
"notes": "Dirt-cheap on eBay. Guided Access locks it to a single app for a kiosk display. No jailbreak required for calendar/dashboard use cases. Older models can run iOS 9.x with vintage Safari."
|
|
96
|
+
}
|
|
97
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { Catalog } from "./schema.js";
|
|
5
|
+
// Resolve repo root relative to this file: dist/catalog/load.js -> ../../
|
|
6
|
+
const repoRoot = () => {
|
|
7
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
return join(here, "..", "..");
|
|
9
|
+
};
|
|
10
|
+
// Parse tags.md and extract the closed-set tag vocabulary.
|
|
11
|
+
// Tags are markdown bullets like "- `tag-name` — description"
|
|
12
|
+
export function parseTags(markdown) {
|
|
13
|
+
const tags = new Set();
|
|
14
|
+
const re = /^-\s+`([a-z0-9-]+)`/gm;
|
|
15
|
+
let match;
|
|
16
|
+
while ((match = re.exec(markdown)) !== null) {
|
|
17
|
+
if (match[1])
|
|
18
|
+
tags.add(match[1]);
|
|
19
|
+
}
|
|
20
|
+
return tags;
|
|
21
|
+
}
|
|
22
|
+
export function loadCatalog() {
|
|
23
|
+
const root = repoRoot();
|
|
24
|
+
const catalogPath = join(root, "catalog.json");
|
|
25
|
+
const tagsPath = join(root, "tags.md");
|
|
26
|
+
const rawCatalog = readFileSync(catalogPath, "utf8");
|
|
27
|
+
const rawTags = readFileSync(tagsPath, "utf8");
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(rawCatalog);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
throw new Error(`catalog.json is not valid JSON: ${e.message}\n` +
|
|
34
|
+
`Path: ${catalogPath}`);
|
|
35
|
+
}
|
|
36
|
+
const result = Catalog.safeParse(parsed);
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
const issues = result.error.issues
|
|
39
|
+
.map((i) => ` - ${i.path.join(".")}: ${i.message}`)
|
|
40
|
+
.join("\n");
|
|
41
|
+
throw new Error(`catalog.json failed schema validation:\n${issues}\n` +
|
|
42
|
+
`Path: ${catalogPath}`);
|
|
43
|
+
}
|
|
44
|
+
const devices = result.data;
|
|
45
|
+
const tags = parseTags(rawTags);
|
|
46
|
+
if (tags.size === 0) {
|
|
47
|
+
throw new Error(`tags.md produced zero tags. Check the markdown format.\nPath: ${tagsPath}`);
|
|
48
|
+
}
|
|
49
|
+
// Validate every catalog tag is in tags.md (hard fail, no warnings).
|
|
50
|
+
const violations = [];
|
|
51
|
+
for (const device of devices) {
|
|
52
|
+
for (const tag of device.idea_fit_tags) {
|
|
53
|
+
if (!tags.has(tag)) {
|
|
54
|
+
violations.push(` - device "${device.id}" uses unknown tag "${tag}"`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (violations.length > 0) {
|
|
59
|
+
throw new Error(`Tag drift detected. Catalog uses tags not declared in tags.md:\n` +
|
|
60
|
+
violations.join("\n") +
|
|
61
|
+
`\n\nFix: add the tag to tags.md, or correct the catalog entry.`);
|
|
62
|
+
}
|
|
63
|
+
// Validate device id uniqueness.
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
for (const device of devices) {
|
|
66
|
+
if (seen.has(device.id)) {
|
|
67
|
+
throw new Error(`Duplicate device id in catalog: "${device.id}"`);
|
|
68
|
+
}
|
|
69
|
+
seen.add(device.id);
|
|
70
|
+
}
|
|
71
|
+
return { devices, tags };
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=load.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/catalog/load.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const Category: z.ZodEnum<["display", "speaker", "input", "sensor", "bulb", "sbc", "handheld", "mini-pc", "wearable", "router", "other"]>;
|
|
3
|
+
export declare const Provenance: z.ZodEnum<["founder-verified", "community-reported", "llm-inferred"]>;
|
|
4
|
+
export declare const CommunitySize: z.ZodEnum<["tiny", "small", "active", "thriving"]>;
|
|
5
|
+
export declare const DeviceEntry: z.ZodObject<{
|
|
6
|
+
id: z.ZodString;
|
|
7
|
+
name: z.ZodString;
|
|
8
|
+
category: z.ZodEnum<["display", "speaker", "input", "sensor", "bulb", "sbc", "handheld", "mini-pc", "wearable", "router", "other"]>;
|
|
9
|
+
idea_fit_tags: z.ZodArray<z.ZodString, "many">;
|
|
10
|
+
hack_difficulty: z.ZodNumber;
|
|
11
|
+
brick_risk: z.ZodNumber;
|
|
12
|
+
brick_provenance: z.ZodEnum<["founder-verified", "community-reported", "llm-inferred"]>;
|
|
13
|
+
last_verified: z.ZodString;
|
|
14
|
+
firmware_links: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
15
|
+
community_size_bucket: z.ZodEnum<["tiny", "small", "active", "thriving"]>;
|
|
16
|
+
notes: z.ZodString;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
category: "display" | "speaker" | "input" | "sensor" | "bulb" | "sbc" | "handheld" | "mini-pc" | "wearable" | "router" | "other";
|
|
21
|
+
idea_fit_tags: string[];
|
|
22
|
+
hack_difficulty: number;
|
|
23
|
+
brick_risk: number;
|
|
24
|
+
brick_provenance: "founder-verified" | "community-reported" | "llm-inferred";
|
|
25
|
+
last_verified: string;
|
|
26
|
+
firmware_links: string[];
|
|
27
|
+
community_size_bucket: "tiny" | "small" | "active" | "thriving";
|
|
28
|
+
notes: string;
|
|
29
|
+
}, {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
category: "display" | "speaker" | "input" | "sensor" | "bulb" | "sbc" | "handheld" | "mini-pc" | "wearable" | "router" | "other";
|
|
33
|
+
idea_fit_tags: string[];
|
|
34
|
+
hack_difficulty: number;
|
|
35
|
+
brick_risk: number;
|
|
36
|
+
brick_provenance: "founder-verified" | "community-reported" | "llm-inferred";
|
|
37
|
+
last_verified: string;
|
|
38
|
+
community_size_bucket: "tiny" | "small" | "active" | "thriving";
|
|
39
|
+
notes: string;
|
|
40
|
+
firmware_links?: string[] | undefined;
|
|
41
|
+
}>;
|
|
42
|
+
export type DeviceEntry = z.infer<typeof DeviceEntry>;
|
|
43
|
+
export type Category = z.infer<typeof Category>;
|
|
44
|
+
export type Provenance = z.infer<typeof Provenance>;
|
|
45
|
+
export declare const Catalog: z.ZodArray<z.ZodObject<{
|
|
46
|
+
id: z.ZodString;
|
|
47
|
+
name: z.ZodString;
|
|
48
|
+
category: z.ZodEnum<["display", "speaker", "input", "sensor", "bulb", "sbc", "handheld", "mini-pc", "wearable", "router", "other"]>;
|
|
49
|
+
idea_fit_tags: z.ZodArray<z.ZodString, "many">;
|
|
50
|
+
hack_difficulty: z.ZodNumber;
|
|
51
|
+
brick_risk: z.ZodNumber;
|
|
52
|
+
brick_provenance: z.ZodEnum<["founder-verified", "community-reported", "llm-inferred"]>;
|
|
53
|
+
last_verified: z.ZodString;
|
|
54
|
+
firmware_links: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
55
|
+
community_size_bucket: z.ZodEnum<["tiny", "small", "active", "thriving"]>;
|
|
56
|
+
notes: z.ZodString;
|
|
57
|
+
}, "strip", z.ZodTypeAny, {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
category: "display" | "speaker" | "input" | "sensor" | "bulb" | "sbc" | "handheld" | "mini-pc" | "wearable" | "router" | "other";
|
|
61
|
+
idea_fit_tags: string[];
|
|
62
|
+
hack_difficulty: number;
|
|
63
|
+
brick_risk: number;
|
|
64
|
+
brick_provenance: "founder-verified" | "community-reported" | "llm-inferred";
|
|
65
|
+
last_verified: string;
|
|
66
|
+
firmware_links: string[];
|
|
67
|
+
community_size_bucket: "tiny" | "small" | "active" | "thriving";
|
|
68
|
+
notes: string;
|
|
69
|
+
}, {
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
category: "display" | "speaker" | "input" | "sensor" | "bulb" | "sbc" | "handheld" | "mini-pc" | "wearable" | "router" | "other";
|
|
73
|
+
idea_fit_tags: string[];
|
|
74
|
+
hack_difficulty: number;
|
|
75
|
+
brick_risk: number;
|
|
76
|
+
brick_provenance: "founder-verified" | "community-reported" | "llm-inferred";
|
|
77
|
+
last_verified: string;
|
|
78
|
+
community_size_bucket: "tiny" | "small" | "active" | "thriving";
|
|
79
|
+
notes: string;
|
|
80
|
+
firmware_links?: string[] | undefined;
|
|
81
|
+
}>, "many">;
|
|
82
|
+
export type Catalog = z.infer<typeof Catalog>;
|
|
83
|
+
export declare const UNRECOVERABLE_BRICK_CATEGORIES: ReadonlySet<Category>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const Category = z.enum([
|
|
3
|
+
"display",
|
|
4
|
+
"speaker",
|
|
5
|
+
"input",
|
|
6
|
+
"sensor",
|
|
7
|
+
"bulb",
|
|
8
|
+
"sbc",
|
|
9
|
+
"handheld",
|
|
10
|
+
"mini-pc",
|
|
11
|
+
"wearable",
|
|
12
|
+
"router",
|
|
13
|
+
"other",
|
|
14
|
+
]);
|
|
15
|
+
export const Provenance = z.enum([
|
|
16
|
+
"founder-verified",
|
|
17
|
+
"community-reported",
|
|
18
|
+
"llm-inferred",
|
|
19
|
+
]);
|
|
20
|
+
export const CommunitySize = z.enum(["tiny", "small", "active", "thriving"]);
|
|
21
|
+
export const DeviceEntry = z.object({
|
|
22
|
+
id: z.string().min(1).regex(/^[a-z0-9-]+$/, "id must be lowercase-kebab-case"),
|
|
23
|
+
name: z.string().min(1),
|
|
24
|
+
category: Category,
|
|
25
|
+
idea_fit_tags: z.array(z.string().min(1)).min(1).max(8),
|
|
26
|
+
hack_difficulty: z.number().int().min(1).max(5),
|
|
27
|
+
brick_risk: z.number().int().min(1).max(5),
|
|
28
|
+
brick_provenance: Provenance,
|
|
29
|
+
last_verified: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "last_verified must be ISO date YYYY-MM-DD"),
|
|
30
|
+
firmware_links: z.array(z.string().url()).default([]),
|
|
31
|
+
community_size_bucket: CommunitySize,
|
|
32
|
+
notes: z.string().max(500, "notes capped at 500 chars to discourage dumping"),
|
|
33
|
+
});
|
|
34
|
+
export const Catalog = z.array(DeviceEntry).min(1);
|
|
35
|
+
// Categories where bricking is unrecoverable. LLM-inferred brick-risk MUST be
|
|
36
|
+
// stripped from output (replaced with "unknown") for these. Hard refusal rule.
|
|
37
|
+
export const UNRECOVERABLE_BRICK_CATEGORIES = new Set([
|
|
38
|
+
"handheld",
|
|
39
|
+
"sbc",
|
|
40
|
+
]);
|
|
41
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/catalog/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAC,IAAI,CAAC;IAC7B,SAAS;IACT,SAAS;IACT,OAAO;IACP,QAAQ;IACR,MAAM;IACN,KAAK;IACL,UAAU;IACV,SAAS;IACT,UAAU;IACV,QAAQ;IACR,OAAO;CACR,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,CAAC;IAC/B,kBAAkB;IAClB,oBAAoB;IACpB,cAAc;CACf,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;AAE7E,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,iCAAiC,CAAC;IAC9E,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,QAAQ,EAAE,QAAQ;IAClB,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/C,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1C,gBAAgB,EAAE,UAAU;IAC5B,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,qBAAqB,EAAE,2CAA2C,CAAC;IACnG,cAAc,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACrD,qBAAqB,EAAE,aAAa;IACpC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,iDAAiD,CAAC;CAC9E,CAAC,CAAC;AAMH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAGnD,8EAA8E;AAC9E,+EAA+E;AAC/E,MAAM,CAAC,MAAM,8BAA8B,GAA0B,IAAI,GAAG,CAAC;IAC3E,UAAU;IACV,KAAK;CACN,CAAC,CAAC"}
|
package/dist/safety.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type DeviceEntry } from "./catalog/schema.js";
|
|
2
|
+
export interface BrickRiskOutput {
|
|
3
|
+
brick_risk: number | null;
|
|
4
|
+
brick_risk_label: string;
|
|
5
|
+
brick_risk_disclaimer: string | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function applyBrickRiskSafety(device: DeviceEntry): BrickRiskOutput;
|
package/dist/safety.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { UNRECOVERABLE_BRICK_CATEGORIES, } from "./catalog/schema.js";
|
|
2
|
+
// Apply the V1 brick-risk safety rule. P0 — tested as a release gate.
|
|
3
|
+
//
|
|
4
|
+
// Rule:
|
|
5
|
+
// - llm-inferred + (handheld | sbc): strip score, replace with "unknown" label
|
|
6
|
+
// - llm-inferred + other categories: keep score, append disclaimer
|
|
7
|
+
// - founder-verified | community-reported: keep score, no disclaimer
|
|
8
|
+
export function applyBrickRiskSafety(device) {
|
|
9
|
+
const isLlmInferred = device.brick_provenance === "llm-inferred";
|
|
10
|
+
const isUnrecoverable = UNRECOVERABLE_BRICK_CATEGORIES.has(device.category);
|
|
11
|
+
if (isLlmInferred && isUnrecoverable) {
|
|
12
|
+
return {
|
|
13
|
+
brick_risk: null,
|
|
14
|
+
brick_risk_label: "brick-risk unknown — research before flashing",
|
|
15
|
+
brick_risk_disclaimer: null,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (isLlmInferred) {
|
|
19
|
+
return {
|
|
20
|
+
brick_risk: device.brick_risk,
|
|
21
|
+
brick_risk_label: `${device.brick_risk}/5 (LLM-inferred, unverified)`,
|
|
22
|
+
brick_risk_disclaimer: "Brick risk for this device is LLM-inferred and unverified. Confirm with the linked community before flashing.",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
brick_risk: device.brick_risk,
|
|
27
|
+
brick_risk_label: `${device.brick_risk}/5 (${device.brick_provenance})`,
|
|
28
|
+
brick_risk_disclaimer: null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safety.js","sourceRoot":"","sources":["../src/safety.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,GAE/B,MAAM,qBAAqB,CAAC;AAQ7B,sEAAsE;AACtE,EAAE;AACF,QAAQ;AACR,iFAAiF;AACjF,sEAAsE;AACtE,uEAAuE;AACvE,MAAM,UAAU,oBAAoB,CAAC,MAAmB;IACtD,MAAM,aAAa,GAAG,MAAM,CAAC,gBAAgB,KAAK,cAAc,CAAC;IACjE,MAAM,eAAe,GAAG,8BAA8B,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAE5E,IAAI,aAAa,IAAI,eAAe,EAAE,CAAC;QACrC,OAAO;YACL,UAAU,EAAE,IAAI;YAChB,gBAAgB,EACd,+CAA+C;YACjD,qBAAqB,EAAE,IAAI;SAC5B,CAAC;IACJ,CAAC;IAED,IAAI,aAAa,EAAE,CAAC;QAClB,OAAO;YACL,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,gBAAgB,EAAE,GAAG,MAAM,CAAC,UAAU,+BAA+B;YACrE,qBAAqB,EACnB,+GAA+G;SAClH,CAAC;IACJ,CAAC;IAED,OAAO;QACL,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,gBAAgB,EAAE,GAAG,MAAM,CAAC,UAAU,OAAO,MAAM,CAAC,gBAAgB,GAAG;QACvE,qBAAqB,EAAE,IAAI;KAC5B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
export declare function sampleJson<T>(opts: {
|
|
3
|
+
server: Server;
|
|
4
|
+
systemPrompt: string;
|
|
5
|
+
userPrompt: string;
|
|
6
|
+
maxTokens?: number;
|
|
7
|
+
}): Promise<T | null>;
|
|
8
|
+
export declare function probeSamplingSupport(server: Server): Promise<boolean>;
|
package/dist/sampling.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Wraps sampling/createMessage with retry-once + degraded-response semantics.
|
|
2
|
+
// Returns either parsed JSON of shape T, or null if the host failed twice.
|
|
3
|
+
//
|
|
4
|
+
// The MCP server NEVER bundles an Anthropic/OpenAI SDK. All reasoning is
|
|
5
|
+
// delegated to the host's LLM via this protocol. Cost: must verify host
|
|
6
|
+
// support via boot probe (see scripts/smoke.ts and src/server.ts boot path).
|
|
7
|
+
export async function sampleJson(opts) {
|
|
8
|
+
const { server, systemPrompt, userPrompt, maxTokens = 1024 } = opts;
|
|
9
|
+
const tryOnce = async () => {
|
|
10
|
+
try {
|
|
11
|
+
const response = await server.createMessage({
|
|
12
|
+
systemPrompt,
|
|
13
|
+
messages: [
|
|
14
|
+
{
|
|
15
|
+
role: "user",
|
|
16
|
+
content: { type: "text", text: userPrompt },
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
maxTokens,
|
|
20
|
+
modelPreferences: {
|
|
21
|
+
// hint to the host: prefer a fast model; reasoning is light here.
|
|
22
|
+
intelligencePriority: 0.6,
|
|
23
|
+
speedPriority: 0.4,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const content = response.content;
|
|
27
|
+
if (content.type !== "text")
|
|
28
|
+
return null;
|
|
29
|
+
const text = content.text.trim();
|
|
30
|
+
// Strip a possible markdown code fence around the JSON.
|
|
31
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
32
|
+
const jsonText = fenced ? fenced[1] : text;
|
|
33
|
+
if (!jsonText)
|
|
34
|
+
return null;
|
|
35
|
+
return JSON.parse(jsonText);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const first = await tryOnce();
|
|
42
|
+
if (first !== null)
|
|
43
|
+
return first;
|
|
44
|
+
// One retry on transient failure.
|
|
45
|
+
const second = await tryOnce();
|
|
46
|
+
return second;
|
|
47
|
+
}
|
|
48
|
+
// Boot-time probe to verify the host actually supports sampling/createMessage.
|
|
49
|
+
// Returns true if the host responded; false if it errored or returned nothing.
|
|
50
|
+
export async function probeSamplingSupport(server) {
|
|
51
|
+
try {
|
|
52
|
+
const response = await server.createMessage({
|
|
53
|
+
systemPrompt: "Respond with the single word: ok. No punctuation, no explanation.",
|
|
54
|
+
messages: [
|
|
55
|
+
{ role: "user", content: { type: "text", text: "probe" } },
|
|
56
|
+
],
|
|
57
|
+
maxTokens: 8,
|
|
58
|
+
});
|
|
59
|
+
return response.content.type === "text";
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=sampling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sampling.js","sourceRoot":"","sources":["../src/sampling.ts"],"names":[],"mappings":"AAEA,8EAA8E;AAC9E,2EAA2E;AAC3E,EAAE;AACF,yEAAyE;AACzE,wEAAwE;AACxE,6EAA6E;AAC7E,MAAM,CAAC,KAAK,UAAU,UAAU,CAAI,IAKnC;IACC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAEpE,MAAM,OAAO,GAAG,KAAK,IAAuB,EAAE;QAC5C,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC;gBAC1C,YAAY;gBACZ,QAAQ,EAAE;oBACR;wBACE,IAAI,EAAE,MAAM;wBACZ,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE;qBAC5C;iBACF;gBACD,SAAS;gBACT,gBAAgB,EAAE;oBAChB,kEAAkE;oBAClE,oBAAoB,EAAE,GAAG;oBACzB,aAAa,EAAE,GAAG;iBACnB;aACF,CAAC,CAAC;YAEH,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;YACjC,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM;gBAAE,OAAO,IAAI,CAAC;YACzC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YAEjC,wDAAwD;YACxD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC;YAC7D,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAC3C,IAAI,CAAC,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAE3B,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAM,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,KAAK,GAAG,MAAM,OAAO,EAAE,CAAC;IAC9B,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAEjC,kCAAkC;IAClC,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAC;IAC/B,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,+EAA+E;AAC/E,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,MAAc;IACvD,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC;YAC1C,YAAY,EACV,mEAAmE;YACrE,QAAQ,EAAE;gBACR,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;aAC3D;YACD,SAAS,EAAE,CAAC;SACb,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,MAAM,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { loadCatalog } from "./catalog/load.js";
|
|
6
|
+
import { probeSamplingSupport } from "./sampling.js";
|
|
7
|
+
import { proposeHardware, proposeHardwareInput, } from "./tools/propose_hardware.js";
|
|
8
|
+
import { assessHackability, assessHackabilityInput, } from "./tools/assess_hackability.js";
|
|
9
|
+
const NAME = "hackshop-mcp";
|
|
10
|
+
const VERSION = "0.0.1";
|
|
11
|
+
async function main() {
|
|
12
|
+
// Boot validation. Refuses to start on bad catalog/tags.
|
|
13
|
+
const { devices, tags } = loadCatalog();
|
|
14
|
+
process.stderr.write(`[hackshop-mcp] Catalog loaded: ${devices.length} devices, ${tags.size} tags.\n`);
|
|
15
|
+
// Note: sampling is a CLIENT capability, not a server one. We don't declare
|
|
16
|
+
// it here; we just call server.createMessage(...) and the host either
|
|
17
|
+
// supports it or doesn't. probeSamplingSupport() handles the check.
|
|
18
|
+
const server = new Server({ name: NAME, version: VERSION }, { capabilities: { tools: {} } });
|
|
19
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
20
|
+
tools: [
|
|
21
|
+
{
|
|
22
|
+
name: "propose_hardware",
|
|
23
|
+
description: "Given a project idea (vague is fine), return 3-5 hardware proposals with 'why this fits', hack difficulty, brick risk (with safety rule applied), firmware links, community size, and a suggested eBay search query string. Compose with ebay-mcp at the host level for live listings.",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
idea: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Project idea, free-form. Vague is OK; the agent will use creativity.",
|
|
30
|
+
},
|
|
31
|
+
budget_usd: {
|
|
32
|
+
type: "number",
|
|
33
|
+
description: "Optional budget in USD. Affects eBay query suggestions.",
|
|
34
|
+
},
|
|
35
|
+
constraints: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "Optional constraints (e.g., 'must be wall-mountable', 'low power').",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
required: ["idea"],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "assess_hackability",
|
|
45
|
+
description: "Given a device name, return: hackable y/n, hack difficulty, brick risk (with safety rule applied), firmware links, community size, last verified date, and notes. Looks up by id, exact name, or substring.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
device_name: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Device name or id.",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
required: ["device_name"],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}));
|
|
59
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
60
|
+
const { name, arguments: args } = request.params;
|
|
61
|
+
if (name === "propose_hardware") {
|
|
62
|
+
const input = proposeHardwareInput.parse(args);
|
|
63
|
+
const out = await proposeHardware(input, devices, server);
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text", text: JSON.stringify(out, null, 2) }],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (name === "assess_hackability") {
|
|
69
|
+
const input = assessHackabilityInput.parse(args);
|
|
70
|
+
const out = assessHackability(input, devices);
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: JSON.stringify(out, null, 2) }],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
76
|
+
});
|
|
77
|
+
const transport = new StdioServerTransport();
|
|
78
|
+
await server.connect(transport);
|
|
79
|
+
// Probe sampling support after connect. Logs to stderr; never fatal.
|
|
80
|
+
// Some hosts (Claude Desktop, Claude Code) support sampling/createMessage;
|
|
81
|
+
// some clients don't. If unsupported, propose_hardware degrades gracefully.
|
|
82
|
+
void probeSamplingSupport(server).then((ok) => {
|
|
83
|
+
if (!ok) {
|
|
84
|
+
process.stderr.write("[hackshop-mcp] Warning: this MCP host does not appear to support " +
|
|
85
|
+
"sampling/createMessage. The propose_hardware tool will return " +
|
|
86
|
+
"degraded responses (raw catalog matches without reasoning). " +
|
|
87
|
+
"Hosts known to support sampling: Claude Desktop, Claude Code.\n");
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
process.stderr.write("[hackshop-mcp] Sampling probe OK.\n");
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
process.stderr.write(`[hackshop-mcp] ${NAME} v${VERSION} ready.\n`);
|
|
94
|
+
}
|
|
95
|
+
main().catch((err) => {
|
|
96
|
+
process.stderr.write(`[hackshop-mcp] Fatal: ${err.message}\n`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
});
|
|
99
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { DeviceEntry } from "../catalog/schema.js";
|
|
3
|
+
export declare const assessHackabilityInput: z.ZodObject<{
|
|
4
|
+
device_name: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
device_name: string;
|
|
7
|
+
}, {
|
|
8
|
+
device_name: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type AssessInput = z.infer<typeof assessHackabilityInput>;
|
|
11
|
+
export interface AssessOutput {
|
|
12
|
+
found: boolean;
|
|
13
|
+
device?: {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
category: string;
|
|
17
|
+
hackable: boolean;
|
|
18
|
+
hack_difficulty: number;
|
|
19
|
+
brick_risk: number | null;
|
|
20
|
+
brick_risk_label: string;
|
|
21
|
+
brick_risk_disclaimer: string | null;
|
|
22
|
+
firmware_links: string[];
|
|
23
|
+
community_size: string;
|
|
24
|
+
last_verified: string;
|
|
25
|
+
notes: string;
|
|
26
|
+
};
|
|
27
|
+
message?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function assessHackability(input: AssessInput, catalog: DeviceEntry[]): AssessOutput;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { applyBrickRiskSafety } from "../safety.js";
|
|
3
|
+
export const assessHackabilityInput = z.object({
|
|
4
|
+
device_name: z.string().min(1).max(200),
|
|
5
|
+
});
|
|
6
|
+
// Simple normalization for fuzzy lookup: lowercase, strip non-alnum.
|
|
7
|
+
function norm(s) {
|
|
8
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
9
|
+
}
|
|
10
|
+
export function assessHackability(input, catalog) {
|
|
11
|
+
const target = norm(input.device_name);
|
|
12
|
+
// Exact match by id, then by normalized name, then substring.
|
|
13
|
+
let match = catalog.find((d) => d.id === input.device_name);
|
|
14
|
+
if (!match)
|
|
15
|
+
match = catalog.find((d) => norm(d.name) === target);
|
|
16
|
+
if (!match)
|
|
17
|
+
match = catalog.find((d) => norm(d.name).includes(target) || target.includes(norm(d.name)));
|
|
18
|
+
if (!match) {
|
|
19
|
+
return {
|
|
20
|
+
found: false,
|
|
21
|
+
message: `"${input.device_name}" is not in the catalog. Cannot assess hackability without verification. Try a more specific name, or check the firmware links section of similar devices.`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const safety = applyBrickRiskSafety(match);
|
|
25
|
+
return {
|
|
26
|
+
found: true,
|
|
27
|
+
device: {
|
|
28
|
+
id: match.id,
|
|
29
|
+
name: match.name,
|
|
30
|
+
category: match.category,
|
|
31
|
+
hackable: true, // presence in catalog implies hackable
|
|
32
|
+
hack_difficulty: match.hack_difficulty,
|
|
33
|
+
brick_risk: safety.brick_risk,
|
|
34
|
+
brick_risk_label: safety.brick_risk_label,
|
|
35
|
+
brick_risk_disclaimer: safety.brick_risk_disclaimer,
|
|
36
|
+
firmware_links: match.firmware_links,
|
|
37
|
+
community_size: match.community_size_bucket,
|
|
38
|
+
last_verified: match.last_verified,
|
|
39
|
+
notes: match.notes,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=assess_hackability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assess_hackability.js","sourceRoot":"","sources":["../../src/tools/assess_hackability.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;CACxC,CAAC,CAAC;AAuBH,qEAAqE;AACrE,SAAS,IAAI,CAAC,CAAS;IACrB,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,KAAkB,EAClB,OAAsB;IAEtB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAEvC,8DAA8D;IAC9D,IAAI,KAAK,GAA4B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,WAAW,CAAC,CAAC;IACrF,IAAI,CAAC,KAAK;QAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,CAAC;IACjE,IAAI,CAAC,KAAK;QAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAExG,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,OAAO,EAAE,IAAI,KAAK,CAAC,WAAW,4JAA4J;SAC3L,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAC;IAE3C,OAAO;QACL,KAAK,EAAE,IAAI;QACX,MAAM,EAAE;YACN,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,QAAQ,EAAE,IAAI,EAAE,uCAAuC;YACvD,eAAe,EAAE,KAAK,CAAC,eAAe;YACtC,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;YACzC,qBAAqB,EAAE,MAAM,CAAC,qBAAqB;YACnD,cAAc,EAAE,KAAK,CAAC,cAAc;YACpC,cAAc,EAAE,KAAK,CAAC,qBAAqB;YAC3C,aAAa,EAAE,KAAK,CAAC,aAAa;YAClC,KAAK,EAAE,KAAK,CAAC,KAAK;SACnB;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import type { DeviceEntry } from "../catalog/schema.js";
|
|
4
|
+
export declare const proposeHardwareInput: z.ZodObject<{
|
|
5
|
+
idea: z.ZodString;
|
|
6
|
+
budget_usd: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
constraints: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
idea: string;
|
|
10
|
+
budget_usd?: number | undefined;
|
|
11
|
+
constraints?: string | undefined;
|
|
12
|
+
}, {
|
|
13
|
+
idea: string;
|
|
14
|
+
budget_usd?: number | undefined;
|
|
15
|
+
constraints?: string | undefined;
|
|
16
|
+
}>;
|
|
17
|
+
export type ProposeInput = z.infer<typeof proposeHardwareInput>;
|
|
18
|
+
export interface Proposal {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
category: string;
|
|
22
|
+
why_this_fits: string;
|
|
23
|
+
hack_difficulty: number;
|
|
24
|
+
brick_risk: number | null;
|
|
25
|
+
brick_risk_label: string;
|
|
26
|
+
brick_risk_disclaimer: string | null;
|
|
27
|
+
firmware_links: string[];
|
|
28
|
+
community_size: string;
|
|
29
|
+
ebay_query_suggestion: string;
|
|
30
|
+
notes: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ProposeOutput {
|
|
33
|
+
proposals: Proposal[];
|
|
34
|
+
reasoning: string;
|
|
35
|
+
degraded: boolean;
|
|
36
|
+
message?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function proposeHardware(input: ProposeInput, catalog: DeviceEntry[], server: Server): Promise<ProposeOutput>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { applyBrickRiskSafety } from "../safety.js";
|
|
3
|
+
import { sampleJson } from "../sampling.js";
|
|
4
|
+
export const proposeHardwareInput = z.object({
|
|
5
|
+
idea: z.string().min(3, "idea is too short").max(2000, "idea is too long"),
|
|
6
|
+
budget_usd: z.number().positive().max(100000).optional(),
|
|
7
|
+
constraints: z.string().max(500).optional(),
|
|
8
|
+
});
|
|
9
|
+
const SYSTEM_PROMPT = `You are a hardware-literate AI scout for tinkerers. The user gives you a project idea (often vague). You pick 3-5 hardware items from the candidate list that creatively fit the idea. Surface options the user wouldn't have thought of. Avoid recommending the obvious choice (Raspberry Pi) unless the idea genuinely calls for it.
|
|
10
|
+
|
|
11
|
+
Return JSON only, no markdown, with this shape:
|
|
12
|
+
{
|
|
13
|
+
"picks": [
|
|
14
|
+
{ "id": "<device id from candidate list>", "why_this_fits": "<one sentence, concrete>" }
|
|
15
|
+
],
|
|
16
|
+
"rationale": "<one paragraph on the overall theme of your picks>"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Rules:
|
|
20
|
+
- Pick 3-5 devices. Never more than 5.
|
|
21
|
+
- Only pick from the candidate list provided. Do not invent devices.
|
|
22
|
+
- "why_this_fits" must mention the user's idea explicitly. No generic praise.
|
|
23
|
+
- If candidates are weak for the idea, say so in the rationale rather than padding with bad picks.`;
|
|
24
|
+
function buildEbayQuery(device, idea) {
|
|
25
|
+
// Suggest an eBay search query the user can pass to ebay-mcp at the host.
|
|
26
|
+
// Avoid LLM model names; use the device's marketing name.
|
|
27
|
+
const cond = device.brick_risk >= 4 ? "working" : "used";
|
|
28
|
+
return `${device.name} ${cond}`;
|
|
29
|
+
}
|
|
30
|
+
function candidateContext(catalog) {
|
|
31
|
+
return catalog
|
|
32
|
+
.map((d) => `- id: ${d.id} | name: ${d.name} | category: ${d.category} | tags: ${d.idea_fit_tags.join(", ")} | notes: ${d.notes}`)
|
|
33
|
+
.join("\n");
|
|
34
|
+
}
|
|
35
|
+
function shortlistCandidates(catalog, idea) {
|
|
36
|
+
// Naive prefilter: if the idea text mentions any tag substring, prefer those.
|
|
37
|
+
// Otherwise return the full catalog (LLM does the real picking).
|
|
38
|
+
const lower = idea.toLowerCase();
|
|
39
|
+
const tagged = catalog.filter((d) => d.idea_fit_tags.some((t) => lower.includes(t.replace("-", " "))));
|
|
40
|
+
if (tagged.length >= 5)
|
|
41
|
+
return tagged;
|
|
42
|
+
return catalog;
|
|
43
|
+
}
|
|
44
|
+
export async function proposeHardware(input, catalog, server) {
|
|
45
|
+
if (catalog.length === 0) {
|
|
46
|
+
return {
|
|
47
|
+
proposals: [],
|
|
48
|
+
reasoning: "Catalog is empty. Cannot propose hardware.",
|
|
49
|
+
degraded: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const candidates = shortlistCandidates(catalog, input.idea);
|
|
53
|
+
const userPrompt = [
|
|
54
|
+
`Project idea: ${input.idea}`,
|
|
55
|
+
input.budget_usd ? `Budget (USD): ${input.budget_usd}` : null,
|
|
56
|
+
input.constraints ? `Constraints: ${input.constraints}` : null,
|
|
57
|
+
"",
|
|
58
|
+
"Candidate hardware:",
|
|
59
|
+
candidateContext(candidates),
|
|
60
|
+
]
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join("\n");
|
|
63
|
+
const sampled = await sampleJson({
|
|
64
|
+
server,
|
|
65
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
66
|
+
userPrompt,
|
|
67
|
+
maxTokens: 1500,
|
|
68
|
+
});
|
|
69
|
+
// Degraded path: sampling failed twice. Return raw shortlist matches with a
|
|
70
|
+
// clear "reasoning unavailable" note. No hallucination, no crash.
|
|
71
|
+
if (!sampled || !Array.isArray(sampled.picks)) {
|
|
72
|
+
const fallback = candidates.slice(0, 3).map((d) => buildProposal(d, input.idea, "Reasoning unavailable; raw catalog match."));
|
|
73
|
+
return {
|
|
74
|
+
proposals: fallback,
|
|
75
|
+
reasoning: "Reasoning unavailable. The host LLM failed twice. Returning raw catalog matches; verify hackability against the linked communities.",
|
|
76
|
+
degraded: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const proposals = [];
|
|
80
|
+
for (const pick of sampled.picks.slice(0, 5)) {
|
|
81
|
+
const device = catalog.find((d) => d.id === pick.id);
|
|
82
|
+
if (!device)
|
|
83
|
+
continue; // sampler hallucinated an id; drop it
|
|
84
|
+
proposals.push(buildProposal(device, input.idea, pick.why_this_fits));
|
|
85
|
+
}
|
|
86
|
+
if (proposals.length === 0) {
|
|
87
|
+
return {
|
|
88
|
+
proposals: [],
|
|
89
|
+
reasoning: sampled.rationale ?? "No catalog match for this idea.",
|
|
90
|
+
degraded: false,
|
|
91
|
+
message: "No catalog devices fit this idea well. Consider expanding the catalog or trying a more concrete description.",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
proposals,
|
|
96
|
+
reasoning: sampled.rationale ?? "",
|
|
97
|
+
degraded: false,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function buildProposal(device, idea, whyThisFits) {
|
|
101
|
+
const safety = applyBrickRiskSafety(device);
|
|
102
|
+
return {
|
|
103
|
+
id: device.id,
|
|
104
|
+
name: device.name,
|
|
105
|
+
category: device.category,
|
|
106
|
+
why_this_fits: whyThisFits,
|
|
107
|
+
hack_difficulty: device.hack_difficulty,
|
|
108
|
+
brick_risk: safety.brick_risk,
|
|
109
|
+
brick_risk_label: safety.brick_risk_label,
|
|
110
|
+
brick_risk_disclaimer: safety.brick_risk_disclaimer,
|
|
111
|
+
firmware_links: device.firmware_links,
|
|
112
|
+
community_size: device.community_size_bucket,
|
|
113
|
+
ebay_query_suggestion: buildEbayQuery(device, idea),
|
|
114
|
+
notes: device.notes,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=propose_hardware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"propose_hardware.js","sourceRoot":"","sources":["../../src/tools/propose_hardware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"idea": "wall-mountable digital art display, sub-$300, supports custom firmware",
|
|
3
|
+
"expected_devices": [
|
|
4
|
+
"electric-objects-eo1"
|
|
5
|
+
],
|
|
6
|
+
"notes": "EO revival regression test (P0 from /office-hours). The single E2E anchor: if the agent doesn't surface the EO frame for this idea, the catalog or the system prompt is broken. Do not ship until this passes."
|
|
7
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"idea": "Always-on family calendar for the kitchen wall. Light colors, no animation, minimalist list of upcoming events for the family (kids' visits, appointments). Don't want a regular LCD monitor — looking for something more like an old e-reader or repurposed tablet.",
|
|
3
|
+
"expected_devices": [
|
|
4
|
+
"barnes-noble-nook-touch",
|
|
5
|
+
"amazon-kindle-paperwhite-gen2",
|
|
6
|
+
"ipad-2"
|
|
7
|
+
],
|
|
8
|
+
"notes": "Founder's first hand-curated example (2026-05-04). Constraints: e-ink preferred for the no-animation/light-colors aesthetic; iPad 2 as cheap kiosk fallback. Per founder research: B&N Nook Touch (Android 2.1, well-documented hack) and Amazon Kindle (older models jailbreakable) are real candidates. The agent should NOT default to Raspberry Pi for this idea — that misses the e-paper / minimalist intent."
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"idea": "REPLACE: one sentence describing your project. Vague is fine. Example: 'I want a meditation app for the bedroom.'",
|
|
3
|
+
"expected_devices": [
|
|
4
|
+
"REPLACE: device id from catalog.json that you'd expect propose_hardware to surface",
|
|
5
|
+
"REPLACE: another device id"
|
|
6
|
+
],
|
|
7
|
+
"notes": "OPTIONAL: why these devices fit, any constraints, what surprised you when you researched manually."
|
|
8
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hackshop-mcp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Hardware-literate AI scout for tinkerers. Idea-to-hardware mapping via MCP.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hackshop-mcp": "./dist/server.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/server.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"catalog.json",
|
|
13
|
+
"tags.md",
|
|
14
|
+
"examples",
|
|
15
|
+
"README.md",
|
|
16
|
+
"SAMPLING.md",
|
|
17
|
+
"CONTRIBUTING.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"dev": "tsx src/server.ts",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"regress": "tsx scripts/regress.ts",
|
|
26
|
+
"validate": "tsx scripts/validate-catalog.ts",
|
|
27
|
+
"smoke": "tsx scripts/smoke.ts",
|
|
28
|
+
"prepublishOnly": "npm run build && npm test && npm run validate"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"mcp",
|
|
32
|
+
"model-context-protocol",
|
|
33
|
+
"hardware",
|
|
34
|
+
"hacking",
|
|
35
|
+
"tinkerer",
|
|
36
|
+
"agent"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"author": "Miguel Sanchez-Grice <msanchezgrice@gmail.com>",
|
|
40
|
+
"homepage": "https://github.com/msanchezgrice/hackshop-mcp",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/msanchezgrice/hackshop-mcp.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/msanchezgrice/hackshop-mcp/issues"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
50
|
+
"zod": "^3.23.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.0.0",
|
|
54
|
+
"tsx": "^4.19.0",
|
|
55
|
+
"typescript": "^5.6.0",
|
|
56
|
+
"vitest": "^2.1.0"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=20"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/tags.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Tag Vocabulary (closed set)
|
|
2
|
+
|
|
3
|
+
These are the only tags valid for `idea_fit_tags` in `catalog.json`. Adding a new tag requires editing this file. The server validates catalog tags against this list at boot — boot fails on drift.
|
|
4
|
+
|
|
5
|
+
## Output / Display
|
|
6
|
+
- `display` — has a screen (any kind)
|
|
7
|
+
- `framed-art` — picture-frame form factor, wall-mount
|
|
8
|
+
- `e-ink` — low-power monochrome / color-e-ink display
|
|
9
|
+
- `ambient` — peripheral visual presence, not foreground
|
|
10
|
+
|
|
11
|
+
## Audio
|
|
12
|
+
- `voice` — input/output for speech
|
|
13
|
+
- `audio-out` — speakers / line-out
|
|
14
|
+
- `drone` — sustained ambient soundscape capable
|
|
15
|
+
|
|
16
|
+
## Input
|
|
17
|
+
- `input` — buttons, dials, sensors as user input
|
|
18
|
+
- `kiosk` — locked-down single-purpose interface
|
|
19
|
+
- `wearable` — body-worn
|
|
20
|
+
|
|
21
|
+
## Sensors / Smart Home
|
|
22
|
+
- `sensor` — environmental sensing (motion, temp, light)
|
|
23
|
+
- `bulb` — controllable lighting
|
|
24
|
+
- `color-cycle` — RGB / programmable color animation
|
|
25
|
+
|
|
26
|
+
## Compute
|
|
27
|
+
- `sbc` — single-board computer (Pi-class)
|
|
28
|
+
- `mini-pc` — x86 small-form-factor
|
|
29
|
+
- `low-power` — battery-friendly, sub-1W ideal
|
|
30
|
+
|
|
31
|
+
## Form / Use
|
|
32
|
+
- `retrogaming` — handheld / console for emulation
|
|
33
|
+
- `handheld` — battery, portable
|
|
34
|
+
- `network` — router/switch repurposing
|
|
35
|
+
- `iot` — small connected device
|
|
36
|
+
- `peripheral` — USB or aux to other compute
|
|
37
|
+
- `educational` — kid/learning oriented
|
|
38
|
+
- `industrial` — heavy-duty, repurposable
|
|
39
|
+
- `creative-coding` — pixel art, generative output
|