@going-haywire/farmhand4claude 0.1.0 → 0.1.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/README.md +2 -2
- package/dist/index.js +59 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,9 +27,9 @@ All logs go to stderr; stdout carries only MCP frames.
|
|
|
27
27
|
|
|
28
28
|
You normally don't run this directly — install the plugin and it runs the proxy:
|
|
29
29
|
|
|
30
|
-
```
|
|
30
|
+
```text
|
|
31
31
|
/plugin marketplace add going-haywire/farmhand4claude
|
|
32
|
-
/plugin install farmhand4claude
|
|
32
|
+
/plugin install farmhand4claude@farmhand-marketplace
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
## Configuration (environment variables)
|
package/dist/index.js
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* - client.setNotificationHandler(ToolListChangedNotificationSchema) — example simpleStreamableHttp.ts
|
|
16
16
|
* - stdio MUST NOT write non-MCP data to stdout (spec/transports) — hence all logs -> stderr
|
|
17
17
|
*/
|
|
18
|
-
import { readFileSync } from "node:fs";
|
|
18
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
|
-
import { resolve } from "node:path";
|
|
20
|
+
import { join, resolve } from "node:path";
|
|
21
21
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
22
22
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
23
23
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -26,30 +26,61 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSche
|
|
|
26
26
|
// ---- config (env-overridable; sensible Haywire defaults) --------------------
|
|
27
27
|
const UPSTREAM_URL = process.env.FARMHAND_URL ?? "http://127.0.0.1:8082/mcp";
|
|
28
28
|
const POLL_MS = Number(process.env.FARMHAND_POLL_MS ?? 2000);
|
|
29
|
+
const TOKEN_REL = join(".haywire", "farmhand_token");
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
-
* `<
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
31
|
+
* Where to look for the bearer token, in priority order. The token lives at
|
|
32
|
+
* `<project>/.haywire/farmhand_token` — the whole game is finding the project.
|
|
33
|
+
*
|
|
34
|
+
* Claude Code does NOT run the MCP server with cwd = workspace (it inherits the
|
|
35
|
+
* launch dir / $HOME — CC issues #17565, #75266), so we do NOT trust
|
|
36
|
+
* process.cwd(). We read the workspace from env vars CC exports to the MCP
|
|
37
|
+
* subprocess (interpolated in plugin.json's env block):
|
|
38
|
+
* FARMHAND_TOKEN_PATH (explicit override) > HAYWIRE_WORKSPACE > CLAUDE_PROJECT_DIR
|
|
39
|
+
* with homedir() as a last resort for manual/standalone runs.
|
|
40
|
+
*
|
|
41
|
+
* Crucially, the onboarding flow scaffolds the project into a SUBDIRECTORY of
|
|
42
|
+
* the opened workspace (e.g. workspace `/testbed`, project `/testbed/demo`), so
|
|
43
|
+
* the token is one level DOWN from the workspace, not directly in it. We
|
|
44
|
+
* therefore check the token under `<base>` AND under each immediate subdir of
|
|
45
|
+
* `<base>`.
|
|
46
|
+
*
|
|
47
|
+
* Computed at CALL TIME, not module load: the studio (and its token) may not
|
|
48
|
+
* exist yet when the proxy starts — the project is created mid-session.
|
|
38
49
|
*/
|
|
39
50
|
function tokenCandidates() {
|
|
40
51
|
const paths = [];
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const push = (p) => {
|
|
54
|
+
const r = resolve(p);
|
|
55
|
+
if (!seen.has(r)) {
|
|
56
|
+
seen.add(r);
|
|
57
|
+
paths.push(r);
|
|
58
|
+
}
|
|
44
59
|
};
|
|
45
60
|
if (process.env.FARMHAND_TOKEN_PATH)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
push(process.env.FARMHAND_TOKEN_PATH);
|
|
62
|
+
const bases = [process.env.HAYWIRE_WORKSPACE, process.env.CLAUDE_PROJECT_DIR, homedir()];
|
|
63
|
+
for (const base of bases) {
|
|
64
|
+
if (!base)
|
|
65
|
+
continue;
|
|
66
|
+
push(join(base, TOKEN_REL)); // CC opened directly in the project
|
|
67
|
+
// …and one level down: CC opened in the parent, project in a subdir.
|
|
68
|
+
for (const child of immediateSubdirs(base))
|
|
69
|
+
push(join(base, child, TOKEN_REL));
|
|
70
|
+
}
|
|
50
71
|
return paths;
|
|
51
72
|
}
|
|
52
|
-
|
|
73
|
+
/** Immediate subdirectory names of `dir` (best-effort; [] on any error). */
|
|
74
|
+
function immediateSubdirs(dir) {
|
|
75
|
+
try {
|
|
76
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
77
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
78
|
+
.map((e) => e.name);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
53
84
|
const log = (...a) => console.error("[farmhand-proxy]", ...a); // stderr only
|
|
54
85
|
// ---- upstream connection state ----------------------------------------------
|
|
55
86
|
let upstream = null;
|
|
@@ -61,7 +92,7 @@ let upstreamResources = [];
|
|
|
61
92
|
* return which path it came from (for diagnostics).
|
|
62
93
|
*/
|
|
63
94
|
function readTokenFrom() {
|
|
64
|
-
for (const p of
|
|
95
|
+
for (const p of tokenCandidates()) {
|
|
65
96
|
try {
|
|
66
97
|
const t = readFileSync(p, "utf8").trim();
|
|
67
98
|
if (t.length > 0)
|
|
@@ -77,7 +108,7 @@ function readToken() {
|
|
|
77
108
|
return readTokenFrom()?.token ?? null;
|
|
78
109
|
}
|
|
79
110
|
/** The stdio-facing server Claude Code talks to. Created first, always up. */
|
|
80
|
-
const proxy = new Server({ name: "farmhand-mcp-server", version: "0.1.
|
|
111
|
+
const proxy = new Server({ name: "farmhand-mcp-server", version: "0.1.1" }, { capabilities: { tools: { listChanged: true }, resources: { listChanged: true } } });
|
|
81
112
|
// Sentinel so the model has an affordance while the studio is down.
|
|
82
113
|
const STATUS_TOOL = {
|
|
83
114
|
name: "farmhand_studio_status",
|
|
@@ -107,7 +138,7 @@ proxy.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
107
138
|
: `Haywire studio not running (no connection to ${UPSTREAM_URL}). ` +
|
|
108
139
|
(found
|
|
109
140
|
? `Token present (${found.path}).`
|
|
110
|
-
: `No token found. Looked in: ${
|
|
141
|
+
: `No token found. Looked in: ${tokenCandidates().join(", ")}.`);
|
|
111
142
|
return { content: [{ type: "text", text }], structuredContent: { up, url: UPSTREAM_URL } };
|
|
112
143
|
}
|
|
113
144
|
if (!upstream) {
|
|
@@ -129,7 +160,7 @@ async function tryConnectUpstream() {
|
|
|
129
160
|
const transport = new StreamableHTTPClientTransport(new URL(UPSTREAM_URL), {
|
|
130
161
|
requestInit: token ? { headers: { Authorization: `Bearer ${token}` } } : undefined,
|
|
131
162
|
});
|
|
132
|
-
const client = new Client({ name: "farmhand-proxy-client", version: "0.1.
|
|
163
|
+
const client = new Client({ name: "farmhand-proxy-client", version: "0.1.1" });
|
|
133
164
|
// Re-emit upstream list_changed across the stdio boundary — the whole point.
|
|
134
165
|
client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
|
|
135
166
|
await refreshTools();
|
|
@@ -211,7 +242,12 @@ async function refreshResources() {
|
|
|
211
242
|
// ---- main -------------------------------------------------------------------
|
|
212
243
|
async function main() {
|
|
213
244
|
await proxy.connect(new StdioServerTransport());
|
|
214
|
-
|
|
245
|
+
const bases = [process.env.HAYWIRE_WORKSPACE, process.env.CLAUDE_PROJECT_DIR, homedir()]
|
|
246
|
+
.filter(Boolean)
|
|
247
|
+
.join(", ");
|
|
248
|
+
log(`up. bridging stdio -> ${UPSTREAM_URL} (poll ${POLL_MS}ms). ` +
|
|
249
|
+
`token search bases (+ their immediate subdirs): ${bases}` +
|
|
250
|
+
(process.env.FARMHAND_TOKEN_PATH ? ` [override: ${process.env.FARMHAND_TOKEN_PATH}]` : ""));
|
|
215
251
|
// Poll so tools auto-appear the moment the studio comes up mid-session,
|
|
216
252
|
// and disappear the moment it dies.
|
|
217
253
|
const tick = async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@going-haywire/farmhand4claude",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Farmhand — the AI harness for Haywire. A stdio↔HTTP MCP proxy that bridges Claude Code to a running Haywire studio, so studio tools appear mid-session with no reconnect.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|