@homespunapps/mcp 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lalit Singh
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,167 @@
1
+ # @homespunapps/mcp
2
+
3
+ A thin **stdio [Model Context Protocol](https://modelcontextprotocol.io) server** for [Homespun](https://github.com/aerolalit/homespun). It lets any MCP client — Claude Desktop, Cursor, Windsurf, Cline, your own host — hand a human a rich interactive UI by URL and get structured data back: forms, approvals, pickers, surveys, dashboards, diff/doc review, multi-step wizards.
4
+
5
+ It is a wrapper, not a reimplementation: all relay I/O goes through [`@homespunapps/core`](https://www.npmjs.com/package/@homespunapps/core), and config is shared with the [`homespun` CLI](https://www.npmjs.com/package/@homespunapps/cli) (`~/.config/homespun/config.json`) — so the CLI and this server use the **same agent identity**.
6
+
7
+ ## Runtime requirement: Node.js >= 20
8
+
9
+ The binary is `homespun-mcp`. It speaks MCP over stdio and is meant to be launched by an MCP host, not run interactively.
10
+
11
+ ## Quickstart
12
+
13
+ No global install needed — point your MCP client at `npx @homespunapps/mcp`. On first use, if no API key is configured, the server auto-registers a fresh agent against the hosted relay and saves the key to the shared CLI store; nothing else to set up.
14
+
15
+ ### Claude Desktop
16
+
17
+ Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "homespun": {
23
+ "command": "npx",
24
+ "args": ["-y", "@homespunapps/mcp"]
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ To pin an existing agent key instead of auto-registering, add an `env` block:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "homespun": {
36
+ "command": "npx",
37
+ "args": ["-y", "@homespunapps/mcp"],
38
+ "env": { "HOMESPUN_API_KEY": "hs_..." }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ### Cursor
45
+
46
+ Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "homespun": {
52
+ "command": "npx",
53
+ "args": ["-y", "@homespunapps/mcp"],
54
+ "env": { "HOMESPUN_API_KEY": "hs_..." }
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ ### Generic MCP host
61
+
62
+ Any client that takes a `command` + `args` + `env` works the same way:
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "homespun": {
68
+ "command": "npx",
69
+ "args": ["-y", "@homespunapps/mcp"],
70
+ "env": {
71
+ "HOMESPUN_URL": "https://homespun.dev",
72
+ "HOMESPUN_API_KEY": "hs_..."
73
+ }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ If you'd rather install it globally (`npm i -g @homespunapps/mcp`), use `"command": "homespun-mcp"` with no `args`.
80
+
81
+ ## Configuration
82
+
83
+ All environment variables are optional — the defaults target the hosted relay and auto-register on first use.
84
+
85
+ | Variable | Default | Purpose |
86
+ | --- | --- | --- |
87
+ | `HOMESPUN_URL` | `https://homespun.dev` | Relay base URL. Set to point at a self-hosted relay. |
88
+ | `HOMESPUN_API_KEY` | _(auto-registered)_ | Agent API key. If unset, the server registers an agent on first use and saves the key to `~/.config/homespun/config.json` (shared with the CLI). |
89
+ | `HOMESPUN_TOKEN` | — | Alias for `HOMESPUN_API_KEY` (for hosts that name secrets `*_TOKEN`). `HOMESPUN_API_KEY` wins if both are set. |
90
+ | `HOMESPUN_AGENT_NAME` | `homespun-mcp` | Display name for the auto-registered agent. |
91
+ | `HOMESPUN_REGISTER_SECRET` | — | Registration secret, only for relays running `REGISTRATION_MODE=secret`. |
92
+
93
+ Config precedence mirrors the CLI: env vars win over the saved profile, which falls back to the default relay URL.
94
+
95
+ ## Tools
96
+
97
+ This server has **full parity with the [`homespun` CLI](https://www.npmjs.com/package/@homespunapps/cli)** — every capability the CLI exposes is reachable here.
98
+
99
+ MCP tools are request/response — there is no long-lived "watch". To receive a human's response you **poll** `get_events` with the cursor from the previous call (optionally with `wait_seconds` to long-poll); to watch a record collection, re-call `list_records` with the prior `since`. Each tool description spells out the pattern for the model.
100
+
101
+ To keep the tool list compact (a flat 50+ tools would bloat client context and degrade selection), **hot-path nouns stay discrete tools** while **multi-verb management nouns collapse into one tool each with a required `action` enum**.
102
+
103
+ ### Hot-path (discrete) tools
104
+
105
+ | Tool | What it does |
106
+ | --- | --- |
107
+ | `create_app` | Create an app — inline HTML (`name`+`html`) **or** reuse a saved template (`template_id`). Optional event/input/record schema, participants, tags, icon, callback, `context_key`. Returns `{ app_id, url, urls, title, expires_at }`. **Give `url` to the human.** |
108
+ | `get_app_state` | Fetch an app's metadata (status, title, expiry) without its event log. |
109
+ | `get_events` | Poll the app's append-only event log for what the human did. Pass `since` (cursor) and optional `wait_seconds` (long-poll). |
110
+ | `send_to_app` | Push an event into an open app to update the live UI. |
111
+ | `update_app` | Edit a live app in place (ttl/title/preamble/input_data/metadata/tags/icon). |
112
+ | `upgrade_app` | Re-pin a live app to another version of its template (swap HTML+schemas, same URL). |
113
+ | `list_apps` | Enumerate your apps (filter by status/template_id; paginated). |
114
+ | `delete_app` | Close/delete an app. |
115
+ | `list_records` | List rows in an app's mutable record collection (also the records poll/watch). |
116
+ | `get_record` | Fetch one record row by key. |
117
+ | `upsert_record` | Create/return a record row (dedups on `record_key`). |
118
+ | `update_record` | Update a record row (optional `if_match` optimistic lock). |
119
+ | `delete_record` | Soft-delete a record row (the page sees it live). |
120
+ | `delete_record_collection` | Drop a whole record collection (all rows + the collection row). Destructive, owner-only, requires `confirm: true`. |
121
+
122
+ ### Consolidated tools (one tool, required `action`)
123
+
124
+ | Tool | Actions |
125
+ | --- | --- |
126
+ | `template` | `create` · `version` · `update` · `search` · `list` · `show` · `get_version` · `delete` · `publish` · `unpublish` · `search_public` · `set_icon` |
127
+ | `template_records` | `list` · `get` · `upsert` · `update` · `delete` · `delete_collection` |
128
+ | `participant` | `list` · `new` · `revoke` |
129
+ | `share` | `list` · `invite` · `set_access` · `revoke` |
130
+ | `attachments` | `upload` · `download` · `show` · `list` · `delete` · `mint_token` · `revoke_token` · `list_tokens` |
131
+ | `taste` | `get` · `set` · `clear` |
132
+ | `key` | `list` · `revoke` |
133
+ | `trash` | `list` · `restore` · `restore_template` · `purge` · `purge_template` |
134
+ | `feedback` | `create` · `list` |
135
+ | `agent` | `whoami` · `claim` · `logout` |
136
+
137
+ ### Single-purpose tools
138
+
139
+ | Tool | What it does |
140
+ | --- | --- |
141
+ | `run_query` | Read-only SQL over your scoped apps/records/events (`format`: json/csv/tsv/table). |
142
+ | `get_skill` | Fetch the relay's auto-updating `SKILL.md` (unauthenticated) to self-teach the workflow. |
143
+
144
+ **Attachments** take/return file paths: `upload` reads an absolute `file_path`; `download` writes to an absolute `out_path` (or returns base64 when omitted).
145
+
146
+ **Events vs records.** Events are an append-only journal — forms, approvals, surveys, pickers. Records are a mutable collection where the current state matters more than the edit history — todo lists, kanban boards, comment threads. Reach for records when the page shows several mutable items.
147
+
148
+ ### Not exposed (and why)
149
+
150
+ - The CLI's `config show` is replaced by `agent`→`whoami` (resolved relay URL + active profile + whether a key is set; no secrets).
151
+ - `agent register` isn't a tool — the server auto-registers on first use and shares the CLI's key store; `agent`→`claim` binds it to a human afterward.
152
+ - `demo` (the interactive 60-second terminal tour) and the CLI self-updater are terminal/CLI concerns with no agent use; omitted.
153
+
154
+ ## Typical flow
155
+
156
+ 1. `create_app` with your HTML + an `event_schema` declaring the events the page emits → returns a `url`.
157
+ 2. Paste the `url` into the conversation and ask the human to open it.
158
+ 3. `get_events` with `wait_seconds: 25` in a loop, passing the prior `next_cursor` as `since`, until the awaited event appears.
159
+ 4. Optionally `send_to_app` to update the live UI, or use the record tools for mutable collections.
160
+
161
+ ## MCP registry
162
+
163
+ `server.json` (in this package) carries the metadata for the [official MCP registry](https://registry.modelcontextprotocol.io). Publishing there is a follow-up step for the maintainer.
164
+
165
+ ## License
166
+
167
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,9 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare const GUIDE_RESOURCE_URI = "homespun://guide";
3
+ export declare const GUIDE_PROMPT_NAME = "homespun_guide";
4
+ /**
5
+ * Register the `homespun_guide` prompt and the `homespun://guide` resource on `server`.
6
+ * `getGuide()` returns the current MCP-flavoured guide markdown (called lazily
7
+ * on each read so a relay can serve an updated guide without re-registering).
8
+ */
9
+ export declare function registerGuideCapabilities(server: McpServer, getGuide: () => string | Promise<string>): void;
@@ -0,0 +1,50 @@
1
+ // Register app's MCP prompt + resource on an McpServer.
2
+ //
3
+ // Both the stdio server (packages/mcp/src/server.ts) and the relay's HTTP MCP
4
+ // server call this so an MCP-native client can discover the conceptual guide
5
+ // without a tool call:
6
+ //
7
+ // - prompt `homespun_guide` — surfaces the guide as a prompt the client can
8
+ // insert into context ("teach me app").
9
+ // - resource `homespun://guide` — the same guide as a readable resource.
10
+ //
11
+ // The guide text is supplied by the host: the relay composes it in-process
12
+ // (MCP-INVOCATION.md + the core extracted from SKILL.md); the stdio server
13
+ // fetches it from the relay over HTTP and falls back to a short pointer when
14
+ // the relay is unreachable at registration time (registration must not block on
15
+ // the network — the get_skill tool is the always-fresh path).
16
+ export const GUIDE_RESOURCE_URI = "homespun://guide";
17
+ export const GUIDE_PROMPT_NAME = "homespun_guide";
18
+ /**
19
+ * Register the `homespun_guide` prompt and the `homespun://guide` resource on `server`.
20
+ * `getGuide()` returns the current MCP-flavoured guide markdown (called lazily
21
+ * on each read so a relay can serve an updated guide without re-registering).
22
+ */
23
+ export function registerGuideCapabilities(server, getGuide) {
24
+ server.registerResource(GUIDE_PROMPT_NAME, GUIDE_RESOURCE_URI, {
25
+ title: "Homespun usage guide",
26
+ description: "The app conceptual guide for MCP clients: when to use app, events vs records, schema design, the house style, and the round-trip mental model — with MCP tool-call invocation grammar.",
27
+ mimeType: "text/markdown",
28
+ }, async () => {
29
+ const text = await getGuide();
30
+ return {
31
+ contents: [
32
+ { uri: GUIDE_RESOURCE_URI, mimeType: "text/markdown", text },
33
+ ],
34
+ };
35
+ });
36
+ server.registerPrompt(GUIDE_PROMPT_NAME, {
37
+ title: "Homespun usage guide",
38
+ description: "Insert the app usage guide (MCP invocation + conceptual core) into the conversation so the model knows how to drive app's tools.",
39
+ }, async () => {
40
+ const text = await getGuide();
41
+ return {
42
+ messages: [
43
+ {
44
+ role: "user",
45
+ content: { type: "text", text },
46
+ },
47
+ ],
48
+ };
49
+ });
50
+ }
@@ -0,0 +1,65 @@
1
+ import { HomespunClient } from "@homespunapps/core";
2
+ /**
3
+ * The hosted Homespun relay — the URL fallback when nothing else is set. A
4
+ * self-hoster overrides it with HOMESPUN_URL or a registered profile.
5
+ */
6
+ export declare const DEFAULT_RELAY_URL = "https://homespun.dev";
7
+ /**
8
+ * Profile name used when this server auto-registers a fresh agent. Matches the
9
+ * CLI's DEFAULT_PROFILE_NAME so the two share the same default identity.
10
+ */
11
+ export declare const DEFAULT_PROFILE_NAME = "default";
12
+ /** Absolute path to the shared CLI/MCP config file (honours XDG_CONFIG_HOME). */
13
+ export declare function storePath(): string;
14
+ /**
15
+ * Clear the active saved profile from the shared store (mirrors `homespun agent
16
+ * logout` for the active-profile case). Removes the profile entry and unsets
17
+ * `current_profile` so the next resolve falls back to env / the default URL.
18
+ * Local-only: it does NOT revoke the key on the relay (use the `key` tool's
19
+ * `revoke` action for that). Idempotent — clearing an empty store is a no-op.
20
+ * Returns the profile name that was cleared (or null when nothing was active)
21
+ * and the store path.
22
+ */
23
+ export declare function clearActiveProfile(): {
24
+ cleared: boolean;
25
+ profile: string | null;
26
+ path: string;
27
+ };
28
+ /** Resolve the relay URL using the same precedence as the CLI. */
29
+ export declare function resolveUrl(): string;
30
+ /**
31
+ * Describe how the server is currently configured WITHOUT touching the network
32
+ * — the resolved relay URL, the active profile name, where the key is coming
33
+ * from, and whether a key is present at all. Backs the `agent` tool's `whoami`
34
+ * action so an MCP client can introspect its own identity / relay binding the
35
+ * way `homespun config show` does for the CLI. No secrets are returned (the API key
36
+ * plaintext is never surfaced — only its source + whether it exists).
37
+ */
38
+ export declare function describeActiveConfig(): {
39
+ url: string;
40
+ profile: string | null;
41
+ api_key_present: boolean;
42
+ api_key_source: "env" | "profile" | "none";
43
+ store_path: string;
44
+ };
45
+ /**
46
+ * Resolve a ready-to-use HomespunClient.
47
+ *
48
+ * First-run setup: if no API key is resolvable from the environment or the
49
+ * shared store, the server auto-registers a fresh agent against the relay and
50
+ * persists the key under the `default` profile in the shared store — so the
51
+ * CLI and any later MCP launch reuse the same identity, and the human never
52
+ * has to run `homespun agent register` by hand.
53
+ *
54
+ * A self-hoster on a `secret`-mode relay (or anyone who prefers explicit
55
+ * provisioning) sets HOMESPUN_API_KEY / HOMESPUN_TOKEN and the auto-register path is
56
+ * never taken.
57
+ *
58
+ * `opts.agentName` labels the auto-registered agent on the relay.
59
+ * `opts.registerSecret` is forwarded as the registration secret for
60
+ * REGISTRATION_MODE=secret relays.
61
+ */
62
+ export declare function resolveClient(opts?: {
63
+ agentName?: string;
64
+ registerSecret?: string;
65
+ }): Promise<HomespunClient>;
package/dist/config.js ADDED
@@ -0,0 +1,229 @@
1
+ // Relay config resolution for the MCP server.
2
+ //
3
+ // Mirrors how `@homespunapps/cli` resolves config (see packages/cli/src/config.ts +
4
+ // store.ts) and shares the SAME on-disk store — ${XDG_CONFIG_HOME or
5
+ // ~/.config}/homespun/config.json — so a key obtained via `homespun agent register`
6
+ // is reused here, and a key obtained here (auto-register on first use) is
7
+ // reused by the CLI.
8
+ //
9
+ // Precedence (highest first):
10
+ // url: HOMESPUN_URL env → active profile's url → DEFAULT_RELAY_URL
11
+ // apiKey: HOMESPUN_API_KEY → HOMESPUN_TOKEN → active profile's api_key
12
+ //
13
+ // HOMESPUN_TOKEN is accepted as an alias for HOMESPUN_API_KEY: MCP host config files
14
+ // (Claude Desktop / Cursor) commonly name secrets "*_TOKEN", and the task
15
+ // brief calls it HOMESPUN_TOKEN. HOMESPUN_API_KEY wins if both are set.
16
+ //
17
+ // The store is read/written WITHOUT a dependency on @homespunapps/cli (it doesn't
18
+ // export its store module). The on-disk shape is kept byte-compatible with
19
+ // the CLI's store.ts so the two stay interchangeable.
20
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
21
+ import { homedir } from "node:os";
22
+ import { join, dirname } from "node:path";
23
+ import { HomespunClient, registerAgent } from "@homespunapps/core";
24
+ /**
25
+ * The hosted Homespun relay — the URL fallback when nothing else is set. A
26
+ * self-hoster overrides it with HOMESPUN_URL or a registered profile.
27
+ */
28
+ export const DEFAULT_RELAY_URL = "https://homespun.dev";
29
+ /**
30
+ * Profile name used when this server auto-registers a fresh agent. Matches the
31
+ * CLI's DEFAULT_PROFILE_NAME so the two share the same default identity.
32
+ */
33
+ export const DEFAULT_PROFILE_NAME = "default";
34
+ /** Absolute path to the shared CLI/MCP config file (honours XDG_CONFIG_HOME). */
35
+ export function storePath() {
36
+ const base = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME.trim() !== ""
37
+ ? process.env.XDG_CONFIG_HOME
38
+ : join(homedir(), ".config");
39
+ return join(base, "homespun", "config.json");
40
+ }
41
+ /**
42
+ * Read the persisted store. Returns an empty store if the file is missing,
43
+ * unparseable, or malformed — mirrors the CLI's tolerant reader so a corrupt
44
+ * file degrades to "no saved profile" instead of crashing.
45
+ */
46
+ function readStore() {
47
+ let text;
48
+ try {
49
+ text = readFileSync(storePath(), "utf8");
50
+ }
51
+ catch {
52
+ return { profiles: {} };
53
+ }
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(text);
57
+ }
58
+ catch {
59
+ return { profiles: {} };
60
+ }
61
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
62
+ return { profiles: {} };
63
+ }
64
+ const obj = parsed;
65
+ if (!obj["profiles"] || typeof obj["profiles"] !== "object") {
66
+ return { profiles: {} };
67
+ }
68
+ const rawProfiles = obj["profiles"];
69
+ const profiles = {};
70
+ for (const [name, raw] of Object.entries(rawProfiles)) {
71
+ if (raw === null || typeof raw !== "object")
72
+ continue;
73
+ const p = raw;
74
+ const profile = {};
75
+ if (typeof p["url"] === "string")
76
+ profile.url = p["url"];
77
+ if (typeof p["api_key"] === "string")
78
+ profile.apiKey = p["api_key"];
79
+ profiles[name] = profile;
80
+ }
81
+ const currentProfile = typeof obj["current_profile"] === "string"
82
+ ? obj["current_profile"]
83
+ : undefined;
84
+ return {
85
+ currentProfile: currentProfile && profiles[currentProfile] !== undefined
86
+ ? currentProfile
87
+ : undefined,
88
+ profiles,
89
+ };
90
+ }
91
+ /** Serialise a Store to the CLI's on-disk JSON shape (snake_case fields). */
92
+ function serialize(store) {
93
+ const profilesOut = {};
94
+ for (const [name, p] of Object.entries(store.profiles)) {
95
+ const o = {};
96
+ if (p.url !== undefined)
97
+ o["url"] = p.url;
98
+ if (p.apiKey !== undefined)
99
+ o["api_key"] = p.apiKey;
100
+ profilesOut[name] = o;
101
+ }
102
+ const body = { profiles: profilesOut };
103
+ if (store.currentProfile !== undefined) {
104
+ body["current_profile"] = store.currentProfile;
105
+ }
106
+ return JSON.stringify(body, null, 2) + "\n";
107
+ }
108
+ /** Upsert one profile and persist (mode 0600). Used by auto-register. */
109
+ function upsertProfile(name, patch, setCurrent) {
110
+ const store = readStore();
111
+ const merged = { ...(store.profiles[name] ?? {}), ...patch };
112
+ store.profiles[name] = merged;
113
+ if (setCurrent || store.currentProfile === undefined) {
114
+ store.currentProfile = name;
115
+ }
116
+ const path = storePath();
117
+ mkdirSync(dirname(path), { recursive: true });
118
+ writeFileSync(path, serialize(store), { mode: 0o600 });
119
+ chmodSync(path, 0o600);
120
+ }
121
+ /**
122
+ * Clear the active saved profile from the shared store (mirrors `homespun agent
123
+ * logout` for the active-profile case). Removes the profile entry and unsets
124
+ * `current_profile` so the next resolve falls back to env / the default URL.
125
+ * Local-only: it does NOT revoke the key on the relay (use the `key` tool's
126
+ * `revoke` action for that). Idempotent — clearing an empty store is a no-op.
127
+ * Returns the profile name that was cleared (or null when nothing was active)
128
+ * and the store path.
129
+ */
130
+ export function clearActiveProfile() {
131
+ const store = readStore();
132
+ const path = storePath();
133
+ const name = store.currentProfile;
134
+ if (name === undefined || store.profiles[name] === undefined) {
135
+ return { cleared: true, profile: null, path };
136
+ }
137
+ delete store.profiles[name];
138
+ store.currentProfile = undefined;
139
+ mkdirSync(dirname(path), { recursive: true });
140
+ writeFileSync(path, serialize(store), { mode: 0o600 });
141
+ chmodSync(path, 0o600);
142
+ return { cleared: true, profile: name, path };
143
+ }
144
+ /** Resolve the relay URL using the same precedence as the CLI. */
145
+ export function resolveUrl() {
146
+ const store = readStore();
147
+ const active = store.currentProfile
148
+ ? store.profiles[store.currentProfile]
149
+ : undefined;
150
+ const url = process.env.HOMESPUN_URL ?? active?.url ?? DEFAULT_RELAY_URL;
151
+ return url.replace(/\/$/, "");
152
+ }
153
+ /**
154
+ * Describe how the server is currently configured WITHOUT touching the network
155
+ * — the resolved relay URL, the active profile name, where the key is coming
156
+ * from, and whether a key is present at all. Backs the `agent` tool's `whoami`
157
+ * action so an MCP client can introspect its own identity / relay binding the
158
+ * way `homespun config show` does for the CLI. No secrets are returned (the API key
159
+ * plaintext is never surfaced — only its source + whether it exists).
160
+ */
161
+ export function describeActiveConfig() {
162
+ const store = readStore();
163
+ const url = resolveUrl();
164
+ const profile = store.currentProfile ?? null;
165
+ let source = "none";
166
+ if ((process.env.HOMESPUN_API_KEY && process.env.HOMESPUN_API_KEY !== "") ||
167
+ (process.env.HOMESPUN_TOKEN && process.env.HOMESPUN_TOKEN !== "")) {
168
+ source = "env";
169
+ }
170
+ else {
171
+ const active = store.currentProfile
172
+ ? store.profiles[store.currentProfile]
173
+ : undefined;
174
+ if (active?.apiKey && active.apiKey !== "")
175
+ source = "profile";
176
+ }
177
+ return {
178
+ url,
179
+ profile,
180
+ api_key_present: source !== "none",
181
+ api_key_source: source,
182
+ store_path: storePath(),
183
+ };
184
+ }
185
+ /** Resolve the API key (env → HOMESPUN_TOKEN alias → active profile). */
186
+ function resolveApiKey() {
187
+ const store = readStore();
188
+ const active = store.currentProfile
189
+ ? store.profiles[store.currentProfile]
190
+ : undefined;
191
+ const key = process.env.HOMESPUN_API_KEY ??
192
+ process.env.HOMESPUN_TOKEN ??
193
+ active?.apiKey;
194
+ return key && key !== "" ? key : undefined;
195
+ }
196
+ /**
197
+ * Resolve a ready-to-use HomespunClient.
198
+ *
199
+ * First-run setup: if no API key is resolvable from the environment or the
200
+ * shared store, the server auto-registers a fresh agent against the relay and
201
+ * persists the key under the `default` profile in the shared store — so the
202
+ * CLI and any later MCP launch reuse the same identity, and the human never
203
+ * has to run `homespun agent register` by hand.
204
+ *
205
+ * A self-hoster on a `secret`-mode relay (or anyone who prefers explicit
206
+ * provisioning) sets HOMESPUN_API_KEY / HOMESPUN_TOKEN and the auto-register path is
207
+ * never taken.
208
+ *
209
+ * `opts.agentName` labels the auto-registered agent on the relay.
210
+ * `opts.registerSecret` is forwarded as the registration secret for
211
+ * REGISTRATION_MODE=secret relays.
212
+ */
213
+ export async function resolveClient(opts = {}) {
214
+ const url = resolveUrl();
215
+ let apiKey = resolveApiKey();
216
+ if (apiKey === undefined) {
217
+ // No key anywhere — provision one and persist it under `default`.
218
+ const result = await registerAgent({
219
+ url,
220
+ name: opts.agentName ?? "homespun-mcp",
221
+ ...(opts.registerSecret !== undefined && opts.registerSecret !== ""
222
+ ? { secret: opts.registerSecret }
223
+ : {}),
224
+ });
225
+ upsertProfile(DEFAULT_PROFILE_NAME, { url, apiKey: result.api_key }, true);
226
+ apiKey = result.api_key;
227
+ }
228
+ return new HomespunClient({ url, apiKey });
229
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Extract every `<!-- homespun:core:start -->…<!-- homespun:core:end -->` block from a
3
+ * SKILL.md body, concatenated in document order (markers removed). Returns the
4
+ * transport-agnostic conceptual core with no CLI command grammar.
5
+ */
6
+ export declare function extractCore(skillMarkdown: string): string;
7
+ /**
8
+ * Build the full MCP guide: the MCP invocation layer followed by the shared
9
+ * conceptual core extracted from SKILL.md. `mcpInvocation` is the contents of
10
+ * skills/homespun/MCP-INVOCATION.md; `skillMarkdown` is the contents of SKILL.md.
11
+ */
12
+ export declare function composeMcpGuide(mcpInvocation: string, skillMarkdown: string): string;
package/dist/guide.js ADDED
@@ -0,0 +1,49 @@
1
+ // Compose the MCP-flavoured app guide from the shared conceptual core + the
2
+ // MCP invocation layer.
3
+ //
4
+ // Single source of truth: the conceptual core lives in skills/homespun/SKILL.md
5
+ // between `<!-- homespun:core:start -->` / `<!-- homespun:core:end -->` markers (the
6
+ // CLI invocation grammar lives OUTSIDE those markers, so the CLI document and
7
+ // the MCP guide share the exact same prose for "when to use app / events vs
8
+ // records / schema design / house style / the round-trip mental model"). The
9
+ // MCP invocation layer (tool-call grammar) lives in skills/homespun/MCP-INVOCATION.md.
10
+ //
11
+ // The MCP guide = MCP-INVOCATION.md (with its trailing "the rest is the core"
12
+ // pointer) + every core block extracted from SKILL.md, in document order. No
13
+ // `homespun ...` command grammar leaks into it.
14
+ //
15
+ // This is pure string manipulation so both the relay (which reads the files at
16
+ // boot and serves the result) and any other consumer can share one
17
+ // implementation without dragging in I/O.
18
+ const CORE_START = "<!-- homespun:core:start -->";
19
+ const CORE_END = "<!-- homespun:core:end -->";
20
+ /**
21
+ * Extract every `<!-- homespun:core:start -->…<!-- homespun:core:end -->` block from a
22
+ * SKILL.md body, concatenated in document order (markers removed). Returns the
23
+ * transport-agnostic conceptual core with no CLI command grammar.
24
+ */
25
+ export function extractCore(skillMarkdown) {
26
+ const blocks = [];
27
+ let cursor = 0;
28
+ for (;;) {
29
+ const start = skillMarkdown.indexOf(CORE_START, cursor);
30
+ if (start === -1)
31
+ break;
32
+ const afterStart = start + CORE_START.length;
33
+ const end = skillMarkdown.indexOf(CORE_END, afterStart);
34
+ if (end === -1)
35
+ break;
36
+ blocks.push(skillMarkdown.slice(afterStart, end).trim());
37
+ cursor = end + CORE_END.length;
38
+ }
39
+ return blocks.join("\n\n");
40
+ }
41
+ /**
42
+ * Build the full MCP guide: the MCP invocation layer followed by the shared
43
+ * conceptual core extracted from SKILL.md. `mcpInvocation` is the contents of
44
+ * skills/homespun/MCP-INVOCATION.md; `skillMarkdown` is the contents of SKILL.md.
45
+ */
46
+ export function composeMcpGuide(mcpInvocation, skillMarkdown) {
47
+ const core = extractCore(skillMarkdown);
48
+ return `${mcpInvocation.trim()}\n\n${core}\n`;
49
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};