@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 +21 -0
- package/README.md +167 -0
- package/dist/capabilities.d.ts +9 -0
- package/dist/capabilities.js +50 -0
- package/dist/config.d.ts +65 -0
- package/dist/config.js +229 -0
- package/dist/guide.d.ts +12 -0
- package/dist/guide.js +49 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +77 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.js +98 -0
- package/dist/skill.d.ts +24 -0
- package/dist/skill.js +82 -0
- package/dist/tools.d.ts +51 -0
- package/dist/tools.js +1029 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/package.json +62 -0
- package/server.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `homespun-mcp` — a thin stdio Model Context Protocol server wrapping Homespun.
|
|
3
|
+
//
|
|
4
|
+
// Speaks MCP over stdio so any MCP client (Claude Desktop, Cursor, …) can use
|
|
5
|
+
// Homespun: create apps, push updates, and poll for the human's response. All
|
|
6
|
+
// relay I/O goes through @homespunapps/core (no duplicated transport logic), and
|
|
7
|
+
// config is shared with the `homespun` CLI (~/.config/homespun/config.json) — so the
|
|
8
|
+
// CLI and this server use the same agent identity.
|
|
9
|
+
//
|
|
10
|
+
// Config (all optional — sensible defaults; auto-registers an agent on first
|
|
11
|
+
// use if no key is found):
|
|
12
|
+
// HOMESPUN_URL relay base URL (default https://homespun.dev)
|
|
13
|
+
// HOMESPUN_API_KEY agent API key (or use the shared CLI store)
|
|
14
|
+
// HOMESPUN_TOKEN alias for HOMESPUN_API_KEY (for MCP host "*_TOKEN" config)
|
|
15
|
+
// HOMESPUN_AGENT_NAME label for the auto-registered agent
|
|
16
|
+
// HOMESPUN_REGISTER_SECRET registration secret (REGISTRATION_MODE=secret relays)
|
|
17
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
|
+
import { buildServer } from "./server.js";
|
|
19
|
+
import { VERSION } from "./version.js";
|
|
20
|
+
async function main() {
|
|
21
|
+
// --version / --help are answered locally without starting the transport, so
|
|
22
|
+
// a human poking at the binary gets a useful response instead of a hung
|
|
23
|
+
// stdio session waiting for JSON-RPC.
|
|
24
|
+
const argv = process.argv.slice(2);
|
|
25
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
26
|
+
process.stdout.write(`homespun-mcp ${VERSION}\n`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
30
|
+
process.stdout.write(HELP);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const server = buildServer({
|
|
34
|
+
agentName: process.env.HOMESPUN_AGENT_NAME,
|
|
35
|
+
registerSecret: process.env.HOMESPUN_REGISTER_SECRET,
|
|
36
|
+
});
|
|
37
|
+
const transport = new StdioServerTransport();
|
|
38
|
+
await server.connect(transport);
|
|
39
|
+
// Stdio MCP servers run until the host closes stdin; keep the process alive.
|
|
40
|
+
// The transport resolves connect() immediately, so without this the event
|
|
41
|
+
// loop would otherwise stay open only because of the stdin reader — which is
|
|
42
|
+
// the intended behaviour. Nothing more to do here.
|
|
43
|
+
}
|
|
44
|
+
const HELP = `homespun-mcp ${VERSION} — Homespun Model Context Protocol server (stdio)
|
|
45
|
+
|
|
46
|
+
Run by an MCP client over stdio; not meant to be invoked interactively. Add it
|
|
47
|
+
to your MCP client config, e.g. Claude Desktop / Cursor:
|
|
48
|
+
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"homespun": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["-y", "@homespunapps/mcp"],
|
|
54
|
+
"env": { "HOMESPUN_API_KEY": "hs_..." }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Environment:
|
|
60
|
+
HOMESPUN_URL Relay base URL (default https://homespun.dev)
|
|
61
|
+
HOMESPUN_API_KEY Agent API key. If unset, the server auto-registers an
|
|
62
|
+
HOMESPUN_TOKEN agent on first use and saves the key to the shared CLI
|
|
63
|
+
store (~/.config/homespun/config.json). HOMESPUN_TOKEN is an
|
|
64
|
+
alias for HOMESPUN_API_KEY.
|
|
65
|
+
HOMESPUN_AGENT_NAME Display name for the auto-registered agent.
|
|
66
|
+
HOMESPUN_REGISTER_SECRET Registration secret (REGISTRATION_MODE=secret relays).
|
|
67
|
+
|
|
68
|
+
Tools exposed: deploy_app, list_rows, get_row, upsert_row, update_row,
|
|
69
|
+
delete_row, get_feed_events, apps, members, attachments, taste, key,
|
|
70
|
+
feedback, agent, get_skill.
|
|
71
|
+
|
|
72
|
+
See https://github.com/aerolalit/homespun for docs.
|
|
73
|
+
`;
|
|
74
|
+
main().catch((e) => {
|
|
75
|
+
process.stderr.write(`homespun-mcp: fatal: ${e instanceof Error ? (e.stack ?? e.message) : String(e)}\n`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { HomespunClient } from "@homespunapps/core";
|
|
3
|
+
export interface BuildServerOptions {
|
|
4
|
+
/** Display name for the auto-registered agent (when no key is configured). */
|
|
5
|
+
agentName?: string;
|
|
6
|
+
/** Registration secret for REGISTRATION_MODE=secret relays. */
|
|
7
|
+
registerSecret?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Inject a pre-built client (tests). When set, the lazy resolver is skipped
|
|
10
|
+
* entirely and no network/store access happens.
|
|
11
|
+
*/
|
|
12
|
+
client?: HomespunClient;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Construct (but do not connect) the Homespun MCP server. Call `.connect(transport)`
|
|
16
|
+
* on the returned server to start serving.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildServer(opts?: BuildServerOptions): McpServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Builds the Homespun MCP server: registers every tool from ./tools.ts against an
|
|
2
|
+
// McpServer and wires each handler to a lazily-resolved HomespunClient.
|
|
3
|
+
//
|
|
4
|
+
// The HomespunClient is resolved ONCE, on the first tool call, then cached — so the
|
|
5
|
+
// (potentially network-touching) auto-register-on-first-use path runs lazily,
|
|
6
|
+
// not at process start. This keeps `initialize` / `tools/list` fast and offline
|
|
7
|
+
// (an MCP host can enumerate the tools without the relay being reachable), and
|
|
8
|
+
// only the first actual tool call provisions a key if needed.
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { resolveClient, resolveUrl } from "./config.js";
|
|
11
|
+
import { TOOLS } from "./tools.js";
|
|
12
|
+
import { VERSION } from "./version.js";
|
|
13
|
+
import { fetchMcpGuide } from "./skill.js";
|
|
14
|
+
import { registerGuideCapabilities } from "./capabilities.js";
|
|
15
|
+
/**
|
|
16
|
+
* Construct (but do not connect) the Homespun MCP server. Call `.connect(transport)`
|
|
17
|
+
* on the returned server to start serving.
|
|
18
|
+
*/
|
|
19
|
+
export function buildServer(opts = {}) {
|
|
20
|
+
const server = new McpServer({
|
|
21
|
+
name: "homespun",
|
|
22
|
+
version: VERSION,
|
|
23
|
+
});
|
|
24
|
+
// Lazily resolve + memoise the client. A failed resolution is not cached, so
|
|
25
|
+
// a transient error (e.g. relay unreachable during auto-register) can be
|
|
26
|
+
// retried on the next tool call.
|
|
27
|
+
let clientPromise;
|
|
28
|
+
const getClient = () => {
|
|
29
|
+
if (opts.client)
|
|
30
|
+
return Promise.resolve(opts.client);
|
|
31
|
+
if (clientPromise === undefined) {
|
|
32
|
+
clientPromise = resolveClient({
|
|
33
|
+
agentName: opts.agentName,
|
|
34
|
+
registerSecret: opts.registerSecret,
|
|
35
|
+
}).catch((e) => {
|
|
36
|
+
clientPromise = undefined;
|
|
37
|
+
throw e;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return clientPromise;
|
|
41
|
+
};
|
|
42
|
+
// MCP consumers get the MCP-flavoured guide (tool-call grammar), not the
|
|
43
|
+
// CLI-grammar SKILL.md. get_skill fetches /skills/homespun/MCP.md from the
|
|
44
|
+
// configured relay; everything else keeps its CLI defaults (the stdio server
|
|
45
|
+
// reads identity from the shared CLI config store).
|
|
46
|
+
const toolEnv = {
|
|
47
|
+
getSkill: (versionOnly) => fetchMcpGuide(resolveUrl(), { version: versionOnly }),
|
|
48
|
+
};
|
|
49
|
+
// Conceptual guide as an MCP prompt + resource. Fetched from the relay lazily
|
|
50
|
+
// on read; a relay-unreachable read surfaces a short pointer to get_skill
|
|
51
|
+
// rather than failing registration.
|
|
52
|
+
registerGuideCapabilities(server, async () => {
|
|
53
|
+
try {
|
|
54
|
+
const { markdown } = await fetchMcpGuide(resolveUrl());
|
|
55
|
+
return markdown ?? "";
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
59
|
+
return ("# app\n\nThe app guide could not be fetched from the relay " +
|
|
60
|
+
`(${message}).\n\nCall the \`get_skill\` tool to retrieve it once the ` +
|
|
61
|
+
"relay is reachable.\n");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
for (const tool of TOOLS) {
|
|
65
|
+
server.registerTool(tool.name, {
|
|
66
|
+
// `title` (top-level, display name) + `annotations` (the ToolAnnotations
|
|
67
|
+
// behavioural hints, which also carry a title) both flow into tools/list
|
|
68
|
+
// so MCP hosts / Anthropic's connector directory can classify the tool.
|
|
69
|
+
title: tool.annotations.title,
|
|
70
|
+
description: tool.description,
|
|
71
|
+
inputSchema: tool.inputSchema,
|
|
72
|
+
annotations: tool.annotations,
|
|
73
|
+
}, async (args) => {
|
|
74
|
+
let client;
|
|
75
|
+
try {
|
|
76
|
+
client = await getClient();
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: "text",
|
|
84
|
+
text: JSON.stringify({
|
|
85
|
+
error: "config_error",
|
|
86
|
+
message,
|
|
87
|
+
hint: "Set HOMESPUN_API_KEY (or HOMESPUN_TOKEN), or ensure the relay at HOMESPUN_URL is reachable so the server can auto-register an agent.",
|
|
88
|
+
}, null, 2),
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
isError: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return tool.handler(client, args, toolEnv);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return server;
|
|
98
|
+
}
|
package/dist/skill.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET the relay's full SKILL.md markdown. `version: true` instead fetches just
|
|
3
|
+
* the relay's reported skill version (the "is my local copy stale?" probe).
|
|
4
|
+
* Throws on a non-2xx or network failure with a message the tool layer can
|
|
5
|
+
* surface.
|
|
6
|
+
*/
|
|
7
|
+
export declare function fetchSkill(relayUrl: string, opts?: {
|
|
8
|
+
version?: boolean;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
markdown?: string;
|
|
11
|
+
version?: string;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* GET the relay's MCP-flavoured guide (the conceptual core + MCP tool-call
|
|
15
|
+
* invocation grammar) from GET /skills/homespun/MCP.md, or just its version from
|
|
16
|
+
* GET /skills/homespun/MCP.md/version. This is what an MCP consumer should read
|
|
17
|
+
* (not the CLI-grammar SKILL.md). Served unauthenticated, same as the skill.
|
|
18
|
+
*/
|
|
19
|
+
export declare function fetchMcpGuide(relayUrl: string, opts?: {
|
|
20
|
+
version?: boolean;
|
|
21
|
+
}): Promise<{
|
|
22
|
+
markdown?: string;
|
|
23
|
+
version?: string;
|
|
24
|
+
}>;
|
package/dist/skill.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Fetch the relay's auto-updating SKILL.md over plain HTTP.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors `homespun skill show|version` (packages/cli/src/commands/skill.ts): the
|
|
4
|
+
// relay serves its skill at GET /skills/homespun/SKILL.md and the version at GET
|
|
5
|
+
// /skills/homespun/SKILL.md/version. Both routes are UNAUTHENTICATED — no API key
|
|
6
|
+
// needed — so an MCP client can self-teach the Homespun workflow before (or without)
|
|
7
|
+
// provisioning a key. We don't go through HomespunClient here precisely because no
|
|
8
|
+
// auth is required and the skill routes are exempt from the version-skew check.
|
|
9
|
+
import { VERSION } from "./version.js";
|
|
10
|
+
/**
|
|
11
|
+
* GET the relay's full SKILL.md markdown. `version: true` instead fetches just
|
|
12
|
+
* the relay's reported skill version (the "is my local copy stale?" probe).
|
|
13
|
+
* Throws on a non-2xx or network failure with a message the tool layer can
|
|
14
|
+
* surface.
|
|
15
|
+
*/
|
|
16
|
+
export async function fetchSkill(relayUrl, opts = {}) {
|
|
17
|
+
const base = relayUrl.replace(/\/$/, "");
|
|
18
|
+
if (opts.version) {
|
|
19
|
+
const target = base + "/skills/homespun/SKILL.md/version";
|
|
20
|
+
const res = await fetchOrThrow(target);
|
|
21
|
+
let body;
|
|
22
|
+
try {
|
|
23
|
+
body = await res.json();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
body = null;
|
|
27
|
+
}
|
|
28
|
+
const version = body !== null &&
|
|
29
|
+
typeof body === "object" &&
|
|
30
|
+
typeof body.version === "string"
|
|
31
|
+
? body.version
|
|
32
|
+
: "0.0.0";
|
|
33
|
+
return { version };
|
|
34
|
+
}
|
|
35
|
+
const target = base + "/skills/homespun/SKILL.md";
|
|
36
|
+
const res = await fetchOrThrow(target);
|
|
37
|
+
const markdown = await res.text();
|
|
38
|
+
return { markdown };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* GET the relay's MCP-flavoured guide (the conceptual core + MCP tool-call
|
|
42
|
+
* invocation grammar) from GET /skills/homespun/MCP.md, or just its version from
|
|
43
|
+
* GET /skills/homespun/MCP.md/version. This is what an MCP consumer should read
|
|
44
|
+
* (not the CLI-grammar SKILL.md). Served unauthenticated, same as the skill.
|
|
45
|
+
*/
|
|
46
|
+
export async function fetchMcpGuide(relayUrl, opts = {}) {
|
|
47
|
+
const base = relayUrl.replace(/\/$/, "");
|
|
48
|
+
if (opts.version) {
|
|
49
|
+
const res = await fetchOrThrow(base + "/skills/homespun/MCP.md/version");
|
|
50
|
+
let body;
|
|
51
|
+
try {
|
|
52
|
+
body = await res.json();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
body = null;
|
|
56
|
+
}
|
|
57
|
+
const version = body !== null &&
|
|
58
|
+
typeof body === "object" &&
|
|
59
|
+
typeof body.version === "string"
|
|
60
|
+
? body.version
|
|
61
|
+
: "0.0.0";
|
|
62
|
+
return { version };
|
|
63
|
+
}
|
|
64
|
+
const res = await fetchOrThrow(base + "/skills/homespun/MCP.md");
|
|
65
|
+
const markdown = await res.text();
|
|
66
|
+
return { markdown };
|
|
67
|
+
}
|
|
68
|
+
async function fetchOrThrow(url) {
|
|
69
|
+
let res;
|
|
70
|
+
try {
|
|
71
|
+
res = await fetch(url, { headers: { "x-homespun-cli-version": VERSION } });
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
75
|
+
throw new Error(`could not reach ${url}: ${msg}`, { cause: e });
|
|
76
|
+
}
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
const body = await res.text().catch(() => "");
|
|
79
|
+
throw new Error(`relay returned ${res.status} for ${url}${body ? ": " + body.slice(0, 200) : ""}`);
|
|
80
|
+
}
|
|
81
|
+
return res;
|
|
82
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import type { HomespunClient } from "@homespunapps/core";
|
|
4
|
+
/**
|
|
5
|
+
* A structured MCP tool result (text content + optional error flag). The
|
|
6
|
+
* index signature keeps it structurally assignable to the SDK's
|
|
7
|
+
* CallToolResult (which carries an open `[x: string]: unknown`).
|
|
8
|
+
*/
|
|
9
|
+
export interface ToolResult {
|
|
10
|
+
content: {
|
|
11
|
+
type: "text";
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
isError?: boolean;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Host-supplied capabilities for the handful of tools that aren't pure
|
|
19
|
+
* HomespunClient wrappers. The stdio server leaves this undefined and the
|
|
20
|
+
* handlers fall back to the CLI config store + a network skill fetch; the
|
|
21
|
+
* relay's HTTP MCP server injects an `env` so those tools resolve against the
|
|
22
|
+
* relay itself (no CLI config on disk, no self-HTTP loop for the skill).
|
|
23
|
+
*
|
|
24
|
+
* This is the single seam that keeps the TOOLS array transport-agnostic and
|
|
25
|
+
* reusable by BOTH servers — every other tool is already a thin HomespunClient
|
|
26
|
+
* call and needs nothing from the host.
|
|
27
|
+
*/
|
|
28
|
+
export interface ToolEnv {
|
|
29
|
+
/** `agent` action=whoami — describe the active identity (no secrets). */
|
|
30
|
+
describeConfig?: () => Record<string, unknown>;
|
|
31
|
+
/** `agent` action=logout — clear the locally-saved profile. */
|
|
32
|
+
clearProfile?: () => Record<string, unknown>;
|
|
33
|
+
/**
|
|
34
|
+
* `get_skill` — return the MCP-flavoured skill markdown + its version. The
|
|
35
|
+
* relay passes its in-process renderer; the stdio server fetches it over
|
|
36
|
+
* HTTP from the relay's /skills route.
|
|
37
|
+
*/
|
|
38
|
+
getSkill?: (versionOnly: boolean) => Promise<{
|
|
39
|
+
markdown?: string;
|
|
40
|
+
version?: string;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
/** One registered tool: name, human/LLM description, Zod input shape, handler. */
|
|
44
|
+
export interface ToolDef {
|
|
45
|
+
name: string;
|
|
46
|
+
description: string;
|
|
47
|
+
inputSchema: z.ZodRawShape;
|
|
48
|
+
annotations: ToolAnnotations;
|
|
49
|
+
handler: (client: HomespunClient, args: Record<string, unknown>, env?: ToolEnv) => Promise<ToolResult>;
|
|
50
|
+
}
|
|
51
|
+
export declare const TOOLS: ToolDef[];
|