@fangorn-network/westmarch 2026.11.3 → 2026.11.4-dev

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 CHANGED
@@ -60,7 +60,7 @@ src/
60
60
  envelope.js · manifest.js · cli.js · view.js · ship.js · eval.ts
61
61
  lint.js …and whether any of that WORKED
62
62
  site/ the stock page every `westmarch-ship` app deploys
63
- plugin/ Claude Code plugin fangorn-index: its skill, fangorn-fx, the fangorn MCP server
63
+ plugin/ Claude Code plugin fangorn-index: its skill and the fangorn MCP server
64
64
  plugin-builder/ Claude Code plugin fangorn-builder: fangorn-app, fangorn-improve
65
65
  examples/ demo/ (a second consumer + fixtures) · publisher/ (the smallest publisher)
66
66
  scripts/ test.js (`npm test`) · build-bin.js (single-file fangorn-mcp)
@@ -12,7 +12,8 @@ browser and any agent can find, verify and query. By the end you have:
12
12
  - **a site**: the view, a page for people, and an MCP endpoint for agents (one Cloudflare
13
13
  Worker, deployed by `westmarch-ship`);
14
14
  - **an agent card** bound to the app on chain (the repo's `@card` record), and registered as an ERC-8004 agent;
15
- - **agents using it:** anyone running `fangorn-mcp` can `open-app` it by name.
15
+ - **agents finding it:** the Fangorn explorer indexes every publisher, so `fangorn-mcp`'s
16
+ `search` finds your records and names you as their publisher.
16
17
 
17
18
  Where the data comes from is up to you. The guide starts from a JSON array of objects,
18
19
  which anything can produce: a scraper, a database export, a converted spreadsheet.
@@ -27,7 +28,7 @@ which anything can produce: a scraper, a database export, a converted spreadshee
27
28
  westmarch-ship ─► card at <site>/.well-known/agent-card.json
28
29
  └─► fangorn card <url> --repo <app> ─► the repo's @card = card URL, + ERC-8004 agent
29
30
  │
30
- fangorn-mcp: open-app ─► reads @card, checks the card points back, searches the view
31
+ the explorer (indexes every publisher) ◄── fangorn-mcp: search, publishers, get, buy
31
32
  ```
32
33
 
33
34
  Everything runs on Base Sepolia: `"network": "baseSepolia"` in `app.json`.
@@ -110,8 +111,7 @@ and commit `.ship/state.json`.
110
111
  - **Serving.** `/view/*` (but `cdn/catalog`) and every MCP tool but `describe` need access: a pass
111
112
  (`POST /pass`, x402 v2, a bearer token good for `days`) or one x402 payment per tool call over
112
113
  x402's MCP transport (`_meta["x402/payment"]`). A call that fails or misses is not charged.
113
- `fangorn-mcp` pays by itself with `FANGORN_MCP_WALLET_KEY`: a pass at or under
114
- `FANGORN_MCP_MAX_PASS`, else per call at or under `FANGORN_MCP_MAX_PRICE`.
114
+ An agent pays with any x402 client pointed at the app's `/mcp`.
115
115
  - **Discovery.** The card's Fangorn extension carries `access` (pass URL and prices), and its
116
116
  skills are tagged `x402`, so the ERC-8004 registration says `x402Support: true`.
117
117
  `/openapi.json` and `/.well-known/x402` list `POST /pass` for x402scan; the facilitator's Bazaar
@@ -177,75 +177,16 @@ or a PR can change the gate along with what it gates.
177
177
  What is not graded yet: a change to how a source *parses*. `--local` reads what the
178
178
  sources last staged; it does not replay their parsing over the raw documents.
179
179
 
180
- ## Using Fangorn apps from an agent
180
+ ## How agents find your data
181
181
 
182
- `fangorn-mcp` is one MCP server for every Fangorn app: a new app needs no new
183
- registration. In Claude Code, the `fangorn-index` plugin installs it with the skill that uses it:
182
+ Through the **Fangorn explorer** (`https://explorer.fangorn.network`), the app that indexes
183
+ every publisher. Once your records are pushed, its next index run (every few minutes) picks
184
+ them up, and anyone running `fangorn-mcp` finds them with `search`, sees your repos under
185
+ `publishers`, and buys what you sell with `buy`. You register nothing for this: being a
186
+ publisher is enough. [docs/mcp.md](mcp.md) has the setup.
184
187
 
185
- ```sh
186
- claude plugin marketplace add fangorn-network/westmarch && claude plugin install fangorn-index@fangorn
187
- ```
188
-
189
- App owners add `fangorn-builder@fangorn` (the `fangorn-app` and `fangorn-improve` skills).
190
- Without the plugin, install it through npm (node ≥ 20):
191
-
192
- ```sh
193
- claude mcp add fangorn -- \
194
- npx -y -p @fangorn-network/westmarch -p @huggingface/transformers fangorn-mcp
195
- ```
196
-
197
- Without `-p @huggingface/transformers`, the install is ~500 MB lighter and `search` ranks by
198
- words, not meaning. For private apps, `FANGORN_MCP_WALLET_KEY` lets it pay
199
- (see [A private app](#a-private-app-the-data-is-the-product)); the plugin reads it from your
200
- environment, while `-e` stores it in `~/.claude.json` in plain text.
201
-
202
- [docs/mcp.md](mcp.md) has the full setup: other MCP clients, every setting, running from
203
- source, and troubleshooting. A single-file binary (`bun build-bin.js`) exists but is not
204
- published yet.
205
-
206
- It starts with three tools:
207
- - **`list-apps`** reads the apps off the chain and keeps the ones whose cards verify.
208
- - **`open-app <name>`** reads that app's `@card` (one call), verifies its card, and adds the
209
- app's tools as `<name>__<tool>`.
210
- - **`call-app-tool`** reaches the same tools, for clients that don't refresh their tool list.
211
-
212
- **Two kinds of tools.**
213
- - **Data tools:** an app whose card names `views` gets these at once, with no browser:
214
- `describe`, `search`, `get`, `similar`, `count`, `browse`. westmarch runs them over the
215
- app's shards, checked against their manifests' sha256. `search` reads the 3 views whose
216
- coverage sketch best matches the query and names the next candidates.
217
- - **Page tools:** `open-app` with `page: true` also opens `card.url` in a browser for the
218
- page's own WebMCP tools. An app with no views gets only these.
219
-
220
- **How fast** (measured on a 60,760-record app in 23 views):
221
-
222
- | | time |
223
- |---|---|
224
- | `open-app` | 0.4–2 s |
225
- | `describe` | under 5 ms |
226
- | first `search`, caches warm | 0.4–1.8 s |
227
- | later searches | ~0.2–0.5 s |
228
- | first `search` from an empty machine (single file) | ~3.5 s, by words while the model downloads |
229
- | page tools (browser) | 5–15 s to open, ~2 GB of memory |
230
-
231
- On a new machine, the embedding model (131 MB, kept in `~/.cache/fangorn-mcp/models`)
232
- downloads in the background. Until it lands, `search` ranks by words and says so in
233
- `ranked_by`; then it switches to meaning. Shards are downloaded once and kept by content
234
- hash.
235
-
236
- **Where the browser comes from (page tools only).** Anything that speaks the Chrome DevTools
237
- Protocol and has WebMCP will do: a headless Chromium service, a hosted browser, or the local
238
- Chrome.
239
-
240
- | setting | browser |
241
- |---|---|
242
- | `FANGORN_MCP_CDP=http://host:9222` (or `--cdp`) | a running browser's DevTools address |
243
- | `FANGORN_MCP_CDP=ws://host:9222/devtools/browser/…` | a browser WebSocket endpoint, which is what hosted services hand out |
244
- | neither | the local Chrome 150+ (`CHROME=/path` if it isn't `google-chrome`), headless (`--headed` to watch) |
245
-
246
- A remote browser is never closed; only the tabs the server opened are. A local Chrome is
247
- shared by every session on the machine and closes when the last one exits. Its profile is
248
- `~/.cache/westmarch-mcp/`. `--from-block <n>` points the server at another deployment.
188
+ Your app's own Worker still serves its page and its own MCP endpoint at `/mcp`, with your
189
+ schema and your tools, for agents that come to your app directly.
249
190
 
250
191
  ## Discovering apps in code
251
192
 
@@ -266,14 +207,6 @@ const fangorn = Fangorn.create({ privateKey: generatePrivateKey() });
266
207
  const { card, publisher, repo } = await fangorn.discoverCard(cardUrl);
267
208
  ```
268
209
 
269
- **From the chain** (every Fangorn app; no index, no server):
270
-
271
- ```js
272
- import { listApps } from "@fangorn-network/westmarch/apps";
273
- const { apps, rejected } = await listApps(fangorn);
274
- // apps: one per repo whose card verifies; rejected: [{ card, why }], reported rather than dropped
275
- ```
276
-
277
210
  **From ERC-8004** (for general-purpose agents that know nothing about Fangorn):
278
211
 
279
212
  ```js
@@ -307,7 +240,6 @@ const { contents } = await fangorn.readRepo(publisher, repo);
307
240
  | card rejected as not bound | URLs must match exactly. A trailing slash, `http` vs `https`, or a different path counts as a different card. |
308
241
  | card rejected after a rebuild | the build dropped the `fangorn` block or changed `fromBlock`/`url`; keep them fixed |
309
242
  | discovery works in Node, fails in a browser | no `Access-Control-Allow-Origin` on the card |
310
- | `listApps` takes forever | `fromBlock` is too early; the scan is 1000 blocks per RPC call |
311
243
  | `fangorn card`: "ERC-8004 registration pins the registration file to IPFS" | no Pinata JWT in `fangorn init` |
312
244
  | `fangorn card` signs as the wrong wallet | `~/.fangorn/config.json` beats `ETH_PRIVATE_KEY` |
313
245
  | `fangorn card`: "replacement transaction underpriced" | a nonce race after the mint; the agent exists, so rerun with `--skip-register` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fangorn-network/westmarch",
3
- "version": "2026.11.3",
3
+ "version": "2026.11.4-dev",
4
4
  "description": "all-in-one builder for fangorn apps",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,7 +11,6 @@
11
11
  "./embed": "./src/core/embed.js",
12
12
  "./rank": "./src/core/rank.js",
13
13
  "./apps": "./src/discover/apps.js",
14
- "./explore": "./src/discover/explore.js",
15
14
  "./agent-card": "./src/agent/agent-card.js",
16
15
  "./view": "./src/publish/view.js",
17
16
  "./roles": "./src/core/roles.js",
@@ -42,7 +41,7 @@
42
41
  "@fangorn-network/markov": "^0.1.0"
43
42
  },
44
43
  "peerDependencies": {
45
- "@fangorn-network/sdk": "^2026.11.3",
44
+ "@fangorn-network/sdk": "^2026.11.4-dev",
46
45
  "@huggingface/transformers": "^4.2.0",
47
46
  "@noble/curves": "^1.9.1",
48
47
  "@semaphore-protocol/core": "^4.14.5",
@@ -57,7 +56,7 @@
57
56
  }
58
57
  },
59
58
  "devDependencies": {
60
- "@fangorn-network/sdk": "2026.11.3",
59
+ "@fangorn-network/sdk": "2026.11.4-dev",
61
60
  "@huggingface/transformers": "^4.2.0",
62
61
  "@noble/curves": "^1.9.1",
63
62
  "@semaphore-protocol/core": "^4.14.5",
@@ -72,8 +71,7 @@
72
71
  "westmarch-ship": "./src/publish/ship.js",
73
72
  "westmarch-eval": "./dist/ts/eval.js",
74
73
  "westmarch-lint": "./src/publish/lint.js",
75
- "fangorn-mcp": "./src/agent/mcp.js",
76
- "fangorn-fx": "./plugin/scripts/fx.mjs"
74
+ "fangorn-mcp": "./src/agent/mcp.js"
77
75
  },
78
76
  "files": [
79
77
  "src",
@@ -85,8 +83,7 @@
85
83
  "site/worker.js",
86
84
  "site/no-model.js",
87
85
  "site/store.js",
88
- "dist/ts",
89
- "plugin/scripts/fx.mjs"
86
+ "dist/ts"
90
87
  ],
91
88
  "engines": {
92
89
  "node": ">=20"
package/site/worker.js CHANGED
@@ -374,8 +374,11 @@ if (typeof process !== "undefined" && import.meta.url === `file://${process.argv
374
374
  for (const t of ["describe", "search", "get", "similar", "count", "browse"]) if (!names.includes(t)) throw new Error(`missing tool ${t}: ${names}`);
375
375
  const described = JSON.parse((await call("tools/call", { name: "describe", arguments: {} })).result.content[0].text);
376
376
  eq(described.views.map((v) => v.rows), [2], "describe reads the catalog from R2");
377
+ eq(described.views[0].repos.map((r) => [r.publisher, r.repo, r.rows]), [[PUB, "shop", 2]], "describe names each repo's publisher");
377
378
  const found = JSON.parse((await call("tools/call", { name: "search", arguments: { query: "reading lamp", limit: 1 } })).result.content[0].text);
378
- eq([found.ranked_by, found.hits[0].title], ["meaning", "Brass lamp"], "search ranks by meaning, the query embedded by the same runtime");
379
+ eq([found.ranked_by, found.hits[0].title, found.hits[0].publisher], ["meaning", "Brass lamp", PUB], "search ranks by meaning, the query embedded by the same runtime; a hit names its publisher");
380
+ const theirs = JSON.parse((await call("tools/call", { name: "search", arguments: { query: "lamp", where: { owner: OWNER } } })).result.content[0].text);
381
+ eq(theirs.hits.length, 0, "search narrows to one publisher with where.owner");
379
382
  if (!calls.at(-1).text[0].startsWith(QUERY)) throw new Error("the query goes to the AI binding with the query prompt");
380
383
  eq((await (await worker.fetch(new Request("https://shop.test/mcp", { method: "POST", body: '{"jsonrpc":"2.0","method":"notifications/initialized"}' }), env)).status), 202, "a notification is accepted, not answered");
381
384