An MCP (Model Context Protocol) server for querying educational resources from AMB (Allgemeines Metadatenprofil für Bildungsressourcen) Nostr relays.
The canonical repository lives on Nostr (NIP-34) — browse it on gitworkshop.dev, or clone with the ngit remote helper:
git clone nostr://laoc.xyz/relay.ngit.dev/amb-mcpMirrors: git.edufeed.org/edufeed/amb-mcp · github.com/edufeed-org/amb-mcp. Issues and PRs are welcome on any of the three — Nostr PRs arrive as pr/* branches.
Releases are tagged (v0.1.0, …) and listed in CHANGELOG.md.
- Cross-content full-text search (
search_content) across educational resources, long-form articles, wikis, transferkiosk projects/measures, and NKBIP-01 scientific publications in one ranked call - Semantic snippet passages from chunk re-ranking surfaced per result when the relay's re-ranking is active
- Full-text search with NIP-50
- Filter by publisher, creator, subject, resource type, educational level
- Browse available subjects, resource types, and educational levels
- Resolve author/organisation names to pubkeys (
resolve_author) for author-scoped queries - NIP-52 calendar event search with temporal, geohash, and hashtag filters
- SKOS controlled-vocabulary lookup and search (
skos_*) - Get individual resources by identifier
- Relay statistics and info
extract_metadata(url, variant, skosSchemes?)— fetch a public web page and produce a complete AMB/EKW form-prefill payload. Returns OpenGraph fallback by default; withANTHROPIC_API_KEYset, an LLM grounded in the configured SKOS vocabularies fills SKOS-typed fields with concept IDs and per-field evidence quotes.- Library export:
import { extractMetadata } from 'amb-mcp/lib'for direct in-process use (e.g. SvelteKit server routes).
- NIP-46 remote signing (bunker) with QR code connection flow
- Sign and publish arbitrary Nostr events
- Create and publish kind 0 (profile/metadata) events
- Create and publish kind 30142 (AMB educational resource) events
- NIP-42 relay authentication support
- Multi-user session isolation
bun installCopy .env.example to .env and configure:
cp .env.example .env| Name | Used by | Default | Description |
|---|---|---|---|
AMB_RELAYS |
all transports | wss://relay.edufeed.org |
Comma-separated AMB relay URLs. Queried by every content tool (search_content, search_resources, get_resource, browse_*, resolve_author, relay_stats) and used as default publish targets for the signing tools. |
AMB_AUTHOR_SETS |
all transports | (empty) | Comma-separated naddr identifiers of NIP-51 follow sets (kind 30000). Loaded once at startup into the author directory served by list_known_authors; the returned pubkeys can then be passed to search_* tools as authors. |
CALENDAR_RELAYS |
all transports | wss://relay.edufeed.org |
Comma-separated NIP-52 calendar relay URLs for search_calendar_events. The AMB relay serves calendar events itself, so the default is the same relay; set this only for split deployments with a dedicated calendar relay. |
CALENDAR_AUTHOR_SETS |
all transports | (empty) | Same as AMB_AUTHOR_SETS, but for the calendar author directory served by list_calendar_authors. |
EDUFEED_APP_BASE_URL |
all transports | (unset) | Frontend base URL (no trailing slash, e.g. https://app.edufeed.org). When set, results from search_content, search_resources, get_resource, and search_calendar_events include a url field pointing at the edufeed-app viewer page (<base>/<naddr>) so LLM clients can render direct links. Unset means no url field. |
SERVER_PRIVATE_KEY |
src/index.ts (+ discovery scripts) |
required for Nostr transport | Nostr private key (nsec or hex) that is the server's own ContextVM identity. The derived pubkey is what clients connect to via cvmi use <pubkey>. Not read by the stdio or HTTP transports. |
RELAYS |
src/index.ts (+ discovery scripts) |
wss://relay.contextvm.org, wss://cvm.otherstuff.ai |
Comma-separated relay URLs for ContextVM transport announcements and request/response traffic. Not read by the stdio or HTTP transports. |
ANTHROPIC_API_KEY |
extract_metadata |
(unset) | Enables LLM-grounded SKOS field extraction. When unset the tool degrades gracefully to OpenGraph/JSON-LD-only output. |
ANTHROPIC_MODEL |
extract_metadata |
claude-sonnet-4-6 |
Override the Anthropic model used for extraction. |
SKOS_SCHEMES |
extract_metadata |
(unset) | JSON map { "<form-field>": "<scheme-uri>" } of default vocabularies used when the caller does not pass skosSchemes explicitly. |
VOCAB_RELAYS |
extract_metadata |
falls back to AMB_RELAYS |
Relays used to resolve naddr1… SKOS scheme identifiers to relay-hosted vocabularies. |
SCHEME_NADDR_* |
extract_metadata |
(unset) | Per-vocabulary naddr overrides (e.g. SCHEME_NADDR_HCRT, SCHEME_NADDR_SCHULFAECHER) mapping well-known scheme URIs to relay-hosted SKOS vocabularies. See .env.example for the full list. |
HTTP_*, OAUTH_* |
src/http.ts only |
see below | HTTP bind and OAuth resource-server settings — documented under Option 4. |
EMBED_TOKEN |
docker-compose.yml only |
(unset) | Token for the embedding service used by the bundled local test relay. Not read by the server itself. |
claude mcp add amb-relay -- bun run /path/to/amb-mcp/src/stdio.tsThis uses the default public relay (wss://relay.edufeed.org). To point at another relay — e.g. the local docker relay from Development — add -e AMB_RELAYS=ws://localhost:3337.
Start the server:
cvmi serve -- bun run src/stdio.tsConnect from another machine:
cvmi use <server-pubkey>bun run src/index.tsFor web-based MCP clients (Claude.ai connectors, MCP Inspector, custom browser apps):
bun run src/http.ts # dev
node dist/http.js # production (after `npm run build`)| Variable | Default | Description |
|---|---|---|
HTTP_PORT |
3000 |
Port to bind. |
HTTP_HOST |
0.0.0.0 |
Bind host. Use 127.0.0.1 to limit to a local proxy. |
HTTP_ALLOWED_HOSTS |
(unset) | Comma-separated Host allow-list. Enables DNS-rebinding protection when set. |
HTTP_ALLOWED_ORIGINS |
(unset) | Comma-separated Origin allow-list. |
OAUTH_ISSUER |
https://auth.edufeed.org/realms/edufeed |
OIDC issuer whose tokens are accepted. |
OAUTH_AUDIENCE |
amb-mcp |
Required aud claim in access tokens. |
OAUTH_JWKS_URI |
<issuer>/protocol/openid-connect/certs |
JWKS endpoint for token signature verification. |
OAUTH_RESOURCE_URL |
https://mcp.amb.edufeed.org/mcp |
Public resource URL advertised in the OAuth protected-resource metadata document. |
Authentication model: the HTTP transport is an OAuth 2.0 resource server.
- A request without an
Authorizationheader gets an anonymous read-only session (mcp:read): search, get, browse, resolve, SKOS lookups. - A request with a valid JWT (issued by
OAUTH_ISSUERfor audienceOAUTH_AUDIENCE) is granted the token's scopes:mcp:readand/ormcp:extract(the budget-spendingextract_metadatatool). An invalid token is rejected with 401. - Write/signing tools are never exposed over HTTP — they are only available on the stdio and Nostr transports. Insufficient scope means the tool is simply absent from
tools/list.
The server exposes:
POST /mcp— JSON-RPC requests (initialize, tool calls, etc.)GET /mcp— server-push SSE stream for the current sessionDELETE /mcp— terminate the current sessionGET /healthz— unauthenticated liveness probe
Example handshake with curl:
# 1. initialize (anonymous = read-only session), capture the Mcp-Session-Id response header
curl -i http://localhost:3000/mcp -X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
# 2. reuse the session id for tools/list, tools/call, etc.
curl http://localhost:3000/mcp -X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Mcp-Session-Id: <id-from-step-1>' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# To use extract_metadata, add an OAuth token to the *initialize* request:
# -H "Authorization: Bearer $ACCESS_TOKEN"
# (scopes are fixed at session init; a token on later requests does not upgrade the session)Smoke-test with MCP Inspector:
npx @modelcontextprotocol/inspector http://localhost:3000/mcpA managed instance is hosted at:
https://mcp.amb.edufeed.org/mcp
It speaks the same streamable-HTTP protocol as the local server. Read tools are public — a request with no Authorization header gets a read-only session (search/get/browse/resolve). The budget-spending extract_metadata tool requires a valid OAuth token carrying the mcp:extract scope; tokens are issued by the Keycloak realm out-of-band — ask the operator. The handshake is otherwise identical to the curl example above; just substitute the URL and drop the Authorization header for read-only use.
Tools are grouped into three profiles:
- Read — search, get, browse, resolve, SKOS lookups, calendar. Available on all transports; served anonymously over HTTP.
- Extract —
extract_metadata. Over HTTP requires an OAuth token with themcp:extractscope. - Write — signer, publish, relay management, and SKOS vocabulary builder tools. Only exposed on the stdio and Nostr transports, never over HTTP.
Topic search across all content types in one ranked call — educational
resources (30142), long-form articles (30023), wikis (30818), transferkiosk
projects (30143), transferkiosk measures (30144), and NKBIP-01 scientific
publications (30040 indices + 30041 sections — academic articles, books).
Results are interleaved and ranked by semantic passage match; each carries
the matched passage (snippet) when the relay's chunk re-ranking is active.
This is the default entry point for natural-language questions.
Parameters:
| Name | Type | Description |
|---|---|---|
query |
string | Free-text topic |
types |
string[] | Subset of ["resource","article","wiki","project","measure","publication"] (default: all) |
language |
string | Label language (default de) |
since / until |
number | Unix timestamp bounds |
authors |
string[] | Author pubkeys (hex) |
limit |
number | Max results, 1-250 (default 20) |
community |
string | Return content shared into this community (hex pubkey or npub) |
Each result: { type, kind, title, url?, naddr?, snippet?, score?, ...type-specific }.
For upcoming events on the same topic, follow up with search_calendar_events.
Facet-in-query syntax: relay-side facets ride inside query as NIP-50
field filters rather than as separate parameters — append them to the
free-text term and the relay resolves them server-side. Examples: type:academic
(publication display type), doi:10.1234/abcd.5678 (bare DOI, no doi: prefix
in the stored value), keywords:<term> (topic words), or partOf:30143:<pubkey>:<d> (publications/measures that
belong to a given project coord). These can be combined with a topic term,
e.g. query: "seminardidaktik partOf:30143:<pubkey>:<d>".
Search for educational resources using full-text search and metadata filters.
Parameters:
| Name | Type | Description |
|---|---|---|
query |
string | Free-text search query |
publisherName |
string | Filter by publisher name |
creatorName |
string | Filter by creator/author name |
subjectLabel |
string | Filter by subject label (e.g., "Mathematik") |
resourceTypeLabel |
string | Filter by resource type (e.g., "Video", "Kurs") |
educationalLevelLabel |
string | Filter by educational level |
language |
string | Language for labels (default: "de") |
authors |
string[] | Filter by author pubkeys (hex) — e.g. from resolve_author or list_known_authors |
since / until |
number | Unix timestamp bounds on resource creation time |
limit |
number | Max results, 1-250 (default: 20) |
Retrieve a single piece of content by naddr, d-tag identifier, or event ID.
An naddr from any search_content result resolves — resources, articles,
wikis, projects, measures, and publications alike; non-resource kinds come
back in the same shape as their search_content result ({ type, kind, title, ... }), not the full AMB resource shape. A bare identifier/eventId
lookup (no naddr) always resolves the full educational-resource metadata
(kind 30142), including creator/publisher and educational properties.
Parameters:
| Name | Type | Description |
|---|---|---|
identifier |
string | Resource d-tag (URL identifier) — resolves kind 30142 only |
author |
string | Author pubkey for disambiguation |
eventId |
string | Direct Nostr event ID lookup — resolves kind 30142 only |
naddr |
string | NIP-19 naddr from a search_content result — preferred; works for any content type |
Each returned resource includes the standard AMB fields plus:
nostr.naddr— NIP-19 addressable identifier (kind=30142, pubkey, d-tag). Useful for any Nostr client.url— direct link to the edufeed-app page for this resource. Only present whenEDUFEED_APP_BASE_URLis configured. LLM clients should cite this as a markdown link ([name](url)) when recommending the resource so users can open it.
Resolve an organisation or person name to candidate pubkeys using the relay's
kind-0 author-profile index (NIP-50 search). This is the entry point for
name-driven questions ("recent articles by Jörg Lohrer"): resolve the name, pick
the best candidate, then pass its pubkey to search_content({ authors: [pubkey] })
or search_calendar_events. Returns up to limit candidates ranked by relevance.
Parameters:
| Name | Type | Description |
|---|---|---|
name |
string | Org or person name to resolve |
limit |
number | Max candidates, 1-25 (default 10) |
List known educational-resource authors loaded from the configured follow sets
(NIP-51 kind 30000, see AMB_AUTHOR_SETS). Returns names, pubkeys, and NIP-05
identifiers. Distinct from resolve_author, which searches the relay-wide
profile index rather than hand-curated sets.
List available subjects/topics with resource counts.
List available learning resource types (Video, Course, Worksheet, etc.).
List available educational levels (Primary, Secondary, Higher Education, etc.).
Get relay information including name, description, and supported NIPs.
List all AMB relays configured for the current session. The write profile
additionally exposes add_relay / remove_relay to adjust the session's relay
set at runtime (per-session over HTTP; process-wide on stdio).
Fetch a user's NIP-65 relay list (kind 10002). See NIP-65 Outbox Model below for the response shape.
Read tools for controlled vocabularies hosted as Nostr events or referenced by URI:
skos_list_vocabularies— list vocabularies known to the serverskos_get_vocabulary/skos_get_vocabulary_status— fetch a scheme with its concepts / check availabilityskos_get_concept— fetch a single concept with labels and relationsskos_search— search concepts across vocabularies by label
The write profile adds a vocabulary builder suite (skos_create_vocabulary,
skos_add_concept, skos_update_concept, skos_remove_concept,
skos_set_relationship, skos_add_mapping, skos_import_turtle,
skos_export_turtle, skos_delete_vocabulary) for authoring SKOS vocabularies
and publishing them as Nostr events.
Fetch one or more public web pages (or PDFs) and produce an AMB/EKW form-prefill
payload. Returns OpenGraph/JSON-LD fallback by default; with ANTHROPIC_API_KEY
set, an LLM grounded in the configured SKOS vocabularies fills SKOS-typed fields
with concept IDs and per-field evidence quotes. Also available as a library
export: import { extractMetadata } from 'amb-mcp/lib'.
Parameters:
| Name | Type | Description |
|---|---|---|
url |
string | Public http(s) URL to extract (use urls for multiple sources) |
urls |
string[] | Multiple source URLs merged into one extraction (e.g. several PDFs) |
variant |
string | Form variant: amb (default), ekw (adds religious-education fields), konfi (adds Konfi-Arbeit fields) |
skosSchemes |
object | Map of form field → SKOS scheme URI, overriding SKOS_SCHEMES |
Fetching is SSRF-aware (private/loopback ranges are blocked).
Search for NIP-52 calendar events (date-based 31922, time-based 31923). Supports temporal filters, geohash location filtering, and hashtag filtering.
Parameters:
| Name | Type | Description |
|---|---|---|
query |
string | Free-text topic. Caveat: when combined with time/geo range filters the relay prioritises the range server-side and ignores this field — for "events about X next week" pass the time range and filter returned events by topic client-side. |
startAfter / startBefore |
number | Unix timestamp bounds for event start |
endAfter / endBefore |
number | Unix timestamp bounds for event end |
geohash |
string | Geohash prefix for location-based search |
hashtags |
string[] | Filter by hashtags |
authors |
string[] | Author pubkeys (hex) |
kinds |
number[] | Event kinds to query (default: [31922, 31923]) |
since / until |
number | Unix timestamp bounds on event creation time |
limit |
number | Max results, 1-250 (default 20) |
Each event carries naddr (NIP-19 addressable identifier) and, when
EDUFEED_APP_BASE_URL is set, url (the edufeed-app viewer at <base>/<naddr>).
Prefer citing url over sourcePage, since the viewer shows fuller event details.
List known calendar event authors loaded from configured follow sets (NIP-51 kind 30000).
Returns author names, pubkeys, and NIP-05 identifiers. Use the returned pubkeys with
search_calendar_events(authors: [...]) to filter events by author.
The MCP server supports signing and publishing Nostr events via NIP-46 remote signing (bunker).
- Call
signer_initto generate a nostrconnect:// URL and QR code - Scan the QR code with your bunker app (Amber, nsecBunker, etc.)
- Call
signer_awaitto wait for the connection to complete
If you have a bunker:// URL from your signer app, use signer_connect directly.
For testing, use signer_connect with an nsec and allowInsecure=true. Never use this in production.
Generate a QR code for connecting a signer app.
Parameters:
| Name | Type | Description |
|---|---|---|
relays |
string[] | Relays for NIP-46 communication (defaults to AMB relays) |
name |
string | Client name shown in the bunker app |
permissions |
string[] | Requested permissions (e.g., ["sign_event:0", "sign_event:30142"]) |
Returns:
sessionId- Session ID forsigner_awaitnostrconnectUrl- The nostrconnect:// URLqrCode- ASCII QR code for terminal display
Wait for a bunker app to connect after scanning the QR code.
Parameters:
| Name | Type | Description |
|---|---|---|
sessionId |
string | Session ID from signer_init |
timeout |
number | Timeout in seconds (default: 120) |
Connect directly using a bunker URL or private key.
Parameters:
| Name | Type | Description |
|---|---|---|
bunkerUrl |
string | bunker:// URL from your signer app |
nsec |
string | Private key (nsec or hex) - development only |
allowInsecure |
boolean | Required when using nsec |
Disconnect the current signer session.
Check the current signer connection status.
Returns:
connected- Whether a signer is connecteduserPubkey- The connected user's public keyconnectedAt- ISO timestamp of connection time
Sign an unsigned Nostr event using the connected signer.
Parameters:
| Name | Type | Description |
|---|---|---|
kind |
number | Event kind (e.g., 0 for metadata, 1 for note) |
content |
string | Event content |
tags |
string[][] | Event tags as array of arrays |
Publish a pre-signed Nostr event to relays.
Parameters:
| Name | Type | Description |
|---|---|---|
event |
object | Signed Nostr event with id, pubkey, created_at, kind, tags, content, sig |
relays |
string[] | Relays to publish to (defaults to AMB relays) |
useOutbox |
boolean | Use NIP-65 outbox model for relay selection (default: true) |
Build, sign, and publish a kind 0 profile metadata event.
Parameters:
| Name | Type | Description |
|---|---|---|
name |
string | Display name |
about |
string | Bio/description |
picture |
string | Avatar URL |
banner |
string | Banner image URL |
nip05 |
string | NIP-05 identifier (e.g., user@domain.com) |
lud16 |
string | Lightning address |
website |
string | Website URL |
relays |
string[] | Relays to publish to |
useOutbox |
boolean | Use NIP-65 outbox model for relay selection (default: true) |
Build, sign, and publish a kind 30142 AMB educational resource event.
Parameters:
| Name | Type | Description |
|---|---|---|
identifier |
string | Unique identifier (d-tag) for the resource |
name |
string | Resource name/title |
description |
string | Resource description |
url |
string | Resource URL |
image |
string | Image/thumbnail URL |
type |
string[] | Resource types (e.g., ["LearningResource", "VideoObject"]) |
inLanguage |
string[] | Language codes (e.g., ["de", "en"]) |
about |
object[] | Subject concepts with id and optional prefLabel |
learningResourceType |
object[] | Learning resource types from HCRT vocabulary |
educationalLevel |
object[] | Educational level concepts |
creator |
object[] | Content creators with name, type, id |
publisher |
object[] | Publishers with name, type, id |
license |
object | License with id (URL) and optional name |
isAccessibleForFree |
boolean | Whether the resource is free to access |
datePublished |
string | Publication date (ISO 8601) |
relays |
string[] | Relays to publish to |
useOutbox |
boolean | Use NIP-65 outbox model for relay selection (default: true) |
Publishing tools use the NIP-65 outbox model by default for intelligent relay selection:
- Author's write relays - Fetched from kind 10002 events
- Tagged users' read relays - For p-tagged mentions, fetches their read relays
- Default relays - Falls back to configured AMB relays
This ensures events are delivered to relays where both the author publishes and where tagged users expect to receive events.
Fetch a user's NIP-65 relay list (kind 10002).
Parameters:
| Name | Type | Description |
|---|---|---|
pubkey |
string | Public key to fetch relay list for (defaults to connected signer) |
Returns:
pubkey- The queried public keyreadRelays- Array of relay URLs marked as readwriteRelays- Array of relay URLs marked as writetotalRead- Count of read relaystotalWrite- Count of write relays
| URI | Description |
|---|---|
amb://schema |
AMB JSON-LD schema |
amb://vocabularies/subjects |
Subject vocabulary |
amb://vocabularies/resource-types |
Learning resource types vocabulary |
amb://vocabularies/educational-levels |
Educational levels vocabulary |
amb://relay-info |
NIP-11 relay information |
The server has three entry points; pick the one that matches your client:
src/index.ts(defaultCMD) — Nostr/ContextVM transport. No HTTP port. Clients reach it by addressing its pubkey on the configured ContextVMRELAYS. RequiresSERVER_PRIVATE_KEY.src/stdio.ts— stdio transport forcvmi serveand Claude Code as a local subprocess.src/http.ts— Streamable HTTP transport onHTTP_PORT(default3000). Use for web-based MCP clients. See Option 4 above for env vars and the handshake.
- Node ≥ 20 (or Bun ≥ 1.1) for runtime.
- Outbound WebSocket access to the configured AMB and ContextVM relays.
- Outbound HTTPS for the
extract_metadatatool (target pages and, optionally, the Anthropic API). - Network access to
git.edufeed.orgduring install — theamb-nostr-converterdependency is fetched as a published tarball from the edufeed npm registry (seepackage.json).
bun install # or: npm install
bun run build # tsc -> dist/
node dist/index.js # production entry; uses Nostr transportFor development without a build step: bun run src/index.ts.
SERVER_PRIVATE_KEYis the server's persistent Nostr identity. Losing or rotating it changes the pubkey clients use to address the server, so treat it as long-lived state. Mint one withnak key generate(or any Nostr keygen) and store it via your secret manager — never commit it.- The matching pubkey is what users pass to
cvmi use <pubkey>. Print it once after first start so operators can record it. ANTHROPIC_API_KEY, if used, should be scoped to this service; theextract_metadatatool will spend tokens on every call where SKOS grounding is requested.
The server itself is stateless on disk — all state lives on the configured relays. The only thing that needs to persist across restarts is the env file containing SERVER_PRIVATE_KEY. No volume is required for the MCP container.
On startup with Nostr transport, the server publishes a ContextVM announcement to RELAYS. To remove an old announcement (e.g. after rotating the key or decommissioning), use scripts/unpublish-server.ts.
- Logging: plain stdout/stderr. Capture via your process supervisor (systemd journal, Docker logs, etc.).
- Healthcheck: the Nostr and stdio entry points have no HTTP healthcheck — liveness ≈ "process is up and the relay subscription has not errored", integrate at the supervisor level. The HTTP entry point exposes
GET /healthz(unauthenticated) for probes. extract_metadataegress: the tool fetches arbitrary URLs supplied by callers. Fetching is SSRF-aware (private/loopback ranges blocked) but you should still consider running it behind an egress proxy if your homelab restricts outbound traffic.- Resource footprint: small — a single Node process with a handful of WebSocket connections. No database, no cache directory.
bun run test # vitest, no network neededdocker-compose.yml in this repo starts a local AMB relay (with Typesense) on
ws://localhost:3337 — it expects a sibling checkout of amb-relay at
../amb-relay for the image build:
docker compose up -d
AMB_RELAY_URL=ws://localhost:3337 bun run scripts/test-client.tsscripts/ask.ts— pose a natural-language question tosearch_contentand print the structured result an LLM client would receivescripts/test-client.ts— quick relay-client smoke test (relay info, queries, transforms)scripts/smoke-search-content.ts/scripts/smoke-extract-metadata.ts— live smoke tests against the dev relayscripts/unpublish-server.ts— remove server from public ContextVM discoveryscripts/delete-announcements.ts— attempt to delete announcement events
src/
├── index.ts # Nostr/ContextVM transport entry point
├── stdio.ts # Stdio transport entry point (for cvmi/Claude Code)
├── http.ts # Streamable HTTP transport entry point (OAuth resource server)
├── session.ts # Per-session MCP server factory (relay clients + tool profile)
├── server-info.ts # Server name/version constants
├── authors.ts # NIP-51 follow-set author directories
├── transport/
│ ├── http.ts # Express app + StreamableHTTPServerTransport wiring
│ ├── auth.ts # JWT verification (JWKS)
│ └── prm.ts # OAuth protected-resource metadata document
├── relay/ # AMB relay client + NIP-50 filter builders
├── calendar/ # NIP-52 calendar filters + transforms
├── content/ # Multi-kind content transforms + snippet handling (search_content)
├── profiles/ # Kind-0 profile transforms (resolve_author)
├── skos/ # SKOS vocabulary client, parser, cache, Nostr loader
├── lib/ # extract_metadata library (fetch, PDF, LLM, vocab grounding)
├── signer/ # NIP-46 signing, publishing, NIP-65 relay lists, event builders
├── tools/ # One module per MCP tool (registered via tools/index.ts profiles)
├── resources/ # amb:// MCP resources (schema, vocabularies, relay info)
├── types/ # AMB TypeScript types
└── utils/ # Event → AMB resource transforms, community helpers
This is free and unencumbered software released into the public domain. See UNLICENSE for details.