@davepi/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/README.md +102 -0
- package/bin/davepi-mcp.js +102 -0
- package/lib/http-proxy.js +210 -0
- package/lib/local-stdio.js +118 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @davepi/mcp
|
|
2
|
+
|
|
3
|
+
One-line MCP wiring for [dAvePi](https://docs.davepi.dev). Connects
|
|
4
|
+
Claude Desktop / Cursor / Claude Code to a dAvePi instance — either
|
|
5
|
+
a remote HTTP `/mcp` endpoint or a local stdio session.
|
|
6
|
+
|
|
7
|
+
## Install / wire it up
|
|
8
|
+
|
|
9
|
+
You don't install — `npx -y` runs the latest published version on
|
|
10
|
+
demand.
|
|
11
|
+
|
|
12
|
+
### Claude Code (`.mcp.json` in the project root)
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"davepi": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "@davepi/mcp"],
|
|
20
|
+
"env": {
|
|
21
|
+
"DAVEPI_URL": "http://localhost:5050",
|
|
22
|
+
"DAVEPI_TOKEN": "<long-lived-jwt>"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Claude Desktop (`claude_desktop_config.json`)
|
|
30
|
+
|
|
31
|
+
macOS path: `~/Library/Application Support/Claude/claude_desktop_config.json`.
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"davepi": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "@davepi/mcp"],
|
|
39
|
+
"env": {
|
|
40
|
+
"DAVEPI_URL": "https://api.example.com",
|
|
41
|
+
"DAVEPI_TOKEN": "<long-lived-jwt>"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Cursor (`.cursor/mcp.json` or settings)
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"davepi": {
|
|
54
|
+
"command": "npx",
|
|
55
|
+
"args": ["-y", "@davepi/mcp"],
|
|
56
|
+
"env": {
|
|
57
|
+
"DAVEPI_URL": "https://api.example.com",
|
|
58
|
+
"DAVEPI_TOKEN": "<long-lived-jwt>"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Modes
|
|
66
|
+
|
|
67
|
+
The wrapper picks one mode based on environment:
|
|
68
|
+
|
|
69
|
+
| Env vars | Mode | Use when |
|
|
70
|
+
|----------|------|----------|
|
|
71
|
+
| `DAVEPI_URL` + `DAVEPI_TOKEN` | HTTP-proxy | dAvePi is hosted (production deployment, demo instance, etc.) and the agent runs on a developer's laptop. |
|
|
72
|
+
| `DAVEPI_SCHEMAS` (or no env vars in a project with a `schema/versions/` directory) | Local-stdio | dAvePi is installed in the same project as the agent's working tree (`npm install davepi`). The wrapper spawns `davepi mcp` and pipes its stdio. |
|
|
73
|
+
|
|
74
|
+
Both modes are pure JSON-RPC pumps — neither holds any MCP-protocol
|
|
75
|
+
state, so a future MCP version doesn't require touching this
|
|
76
|
+
package.
|
|
77
|
+
|
|
78
|
+
## Issuing a long-lived JWT for `DAVEPI_TOKEN`
|
|
79
|
+
|
|
80
|
+
Run this on the dAvePi server (or anywhere with `TOKEN_KEY`):
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
node -e '
|
|
84
|
+
const jwt = require("jsonwebtoken");
|
|
85
|
+
console.log(jwt.sign(
|
|
86
|
+
{ user_id: "<your-user-id>", roles: ["user"] },
|
|
87
|
+
process.env.TOKEN_KEY,
|
|
88
|
+
{ expiresIn: "30d" }
|
|
89
|
+
));
|
|
90
|
+
'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Treat the result like any other API credential.
|
|
94
|
+
|
|
95
|
+
## Documentation
|
|
96
|
+
|
|
97
|
+
- [Surfaces → MCP server](https://docs.davepi.dev/surfaces/mcp/) — the full MCP tool surface dAvePi exposes.
|
|
98
|
+
- [Concepts → Why agents come first](https://docs.davepi.dev/concepts/agent-first/) — why MCP is first-class.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
ISC. Source: <https://github.com/projik/davepi/tree/main/packages/mcp>.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @davepi/mcp — one-line MCP wiring for a dAvePi instance.
|
|
4
|
+
*
|
|
5
|
+
* Two modes, picked by environment:
|
|
6
|
+
*
|
|
7
|
+
* HTTP-proxy (DAVEPI_URL set)
|
|
8
|
+
* The wrapper runs as the agent's MCP server (stdio JSON-RPC)
|
|
9
|
+
* and forwards every message to the remote dAvePi's /mcp HTTP
|
|
10
|
+
* endpoint with `Authorization: Bearer ${DAVEPI_TOKEN}`. Use
|
|
11
|
+
* when the dAvePi instance is hosted (demo.davepi.dev,
|
|
12
|
+
* production deployment, etc.) and the agent runs on a
|
|
13
|
+
* developer's laptop.
|
|
14
|
+
*
|
|
15
|
+
* Local-stdio (DAVEPI_SCHEMAS set, DAVEPI_URL unset)
|
|
16
|
+
* The wrapper spawns `davepi mcp` from the project's locally
|
|
17
|
+
* installed `davepi` package and pipes its stdio. Use when
|
|
18
|
+
* dAvePi is installed in the same project as the agent's
|
|
19
|
+
* working tree. This mode requires the project to have
|
|
20
|
+
* `davepi` as a dependency; we surface a helpful error if the
|
|
21
|
+
* binary can't be found.
|
|
22
|
+
*
|
|
23
|
+
* Both modes are pure stdio JSON-RPC pumps — neither holds any
|
|
24
|
+
* MCP-protocol state of its own, so a future MCP version doesn't
|
|
25
|
+
* require touching this package.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
const path = require('path');
|
|
31
|
+
|
|
32
|
+
const HELP = `Usage: davepi-mcp
|
|
33
|
+
|
|
34
|
+
Pick one mode by environment:
|
|
35
|
+
|
|
36
|
+
HTTP-proxy mode:
|
|
37
|
+
DAVEPI_URL URL of the dAvePi server (e.g. https://api.example.com)
|
|
38
|
+
DAVEPI_TOKEN Long-lived JWT issued by /login on that server
|
|
39
|
+
|
|
40
|
+
Local-stdio mode:
|
|
41
|
+
DAVEPI_SCHEMAS Path to schema/versions/ (defaults to ./schema/versions)
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
|
|
45
|
+
# Wire an agent to a hosted dAvePi:
|
|
46
|
+
DAVEPI_URL=https://api.example.com DAVEPI_TOKEN=eyJ... npx -y @davepi/mcp
|
|
47
|
+
|
|
48
|
+
# Wire an agent to local schemas (project must have \`davepi\` installed):
|
|
49
|
+
DAVEPI_SCHEMAS=./schema/versions npx -y @davepi/mcp
|
|
50
|
+
|
|
51
|
+
Documentation: https://docs.davepi.dev/surfaces/mcp/
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
function err(msg) {
|
|
55
|
+
process.stderr.write(`davepi-mcp: ${msg}\n`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function help() {
|
|
59
|
+
process.stderr.write(HELP);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function main() {
|
|
63
|
+
const args = process.argv.slice(2);
|
|
64
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
65
|
+
help();
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const url = process.env.DAVEPI_URL;
|
|
70
|
+
const schemas = process.env.DAVEPI_SCHEMAS;
|
|
71
|
+
|
|
72
|
+
if (url) {
|
|
73
|
+
if (!process.env.DAVEPI_TOKEN) {
|
|
74
|
+
err('DAVEPI_URL is set but DAVEPI_TOKEN is missing. Set both for HTTP-proxy mode.');
|
|
75
|
+
help();
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
const { runHttpProxy } = require(path.join('..', 'lib', 'http-proxy.js'));
|
|
79
|
+
return runHttpProxy({
|
|
80
|
+
url,
|
|
81
|
+
token: process.env.DAVEPI_TOKEN,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (schemas || args.length === 0) {
|
|
86
|
+
// Local-stdio mode. Schemas path defaults to ./schema/versions
|
|
87
|
+
// (the davepi convention) so a user with a project structured
|
|
88
|
+
// the standard way can omit DAVEPI_SCHEMAS entirely.
|
|
89
|
+
const { runLocalStdio } = require(path.join('..', 'lib', 'local-stdio.js'));
|
|
90
|
+
return runLocalStdio({
|
|
91
|
+
schemas: schemas || path.join(process.cwd(), 'schema', 'versions'),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
help();
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main().catch((e) => {
|
|
100
|
+
err(e && e.stack ? e.stack : String(e));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP-proxy mode: bridge stdio JSON-RPC ↔ remote /mcp HTTP endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Why a manual stdio loop instead of the MCP SDK's bridge classes:
|
|
5
|
+
* the MCP protocol is JSON-RPC 2.0 with specific method names. The
|
|
6
|
+
* wrapper doesn't need to understand any of the methods — it just
|
|
7
|
+
* forwards bytes — so importing the SDK would add ~MB of code to
|
|
8
|
+
* pump line-delimited JSON through `fetch`. The dAvePi server's
|
|
9
|
+
* /mcp endpoint runs StreamableHTTPServerTransport in stateless
|
|
10
|
+
* mode, which means each POST is one request-response pair with
|
|
11
|
+
* a JSON body.
|
|
12
|
+
*
|
|
13
|
+
* Each line on stdin is one JSON-RPC message from the agent. We
|
|
14
|
+
* POST it to ${url}/mcp with the bearer token and write the
|
|
15
|
+
* response body to stdout, terminated by a newline (the SDK's
|
|
16
|
+
* stdio transport is line-delimited).
|
|
17
|
+
*
|
|
18
|
+
* Errors surface to the agent as JSON-RPC error responses so the
|
|
19
|
+
* agent sees an actionable failure instead of a silent hang. When
|
|
20
|
+
* the upstream is unreachable we keep the loop alive — the user
|
|
21
|
+
* may have a transient network blip, and exiting would force the
|
|
22
|
+
* agent to restart the proxy.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const readline = require('node:readline');
|
|
28
|
+
|
|
29
|
+
function buildErrorResponse(reqId, code, message, data) {
|
|
30
|
+
// JSON-RPC 2.0 error envelope. -32000 to -32099 is the reserved
|
|
31
|
+
// server-implementation range; we use -32000 for transport errors
|
|
32
|
+
// so a downstream agent can branch on "talk to operator" vs
|
|
33
|
+
// "fix your call".
|
|
34
|
+
return JSON.stringify({
|
|
35
|
+
jsonrpc: '2.0',
|
|
36
|
+
id: reqId === undefined ? null : reqId,
|
|
37
|
+
error: { code, message, ...(data !== undefined ? { data } : {}) },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Default upstream timeout. Long enough that a slow aggregation
|
|
42
|
+
// against a remote /mcp doesn't false-positive, short enough that a
|
|
43
|
+
// stalled connection doesn't block the agent indefinitely. Tunable
|
|
44
|
+
// via DAVEPI_HTTP_TIMEOUT_MS for CI / dev / latency-sensitive setups.
|
|
45
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
46
|
+
|
|
47
|
+
function getTimeoutMs() {
|
|
48
|
+
const raw = process.env.DAVEPI_HTTP_TIMEOUT_MS;
|
|
49
|
+
if (!raw) return DEFAULT_TIMEOUT_MS;
|
|
50
|
+
const parsed = Number(raw);
|
|
51
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_TIMEOUT_MS;
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function forwardOne(line, { url, token }) {
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(line);
|
|
59
|
+
} catch (parseErr) {
|
|
60
|
+
return buildErrorResponse(null, -32700, `Parse error: ${parseErr.message}`);
|
|
61
|
+
}
|
|
62
|
+
const reqId = parsed && Object.prototype.hasOwnProperty.call(parsed, 'id') ? parsed.id : undefined;
|
|
63
|
+
|
|
64
|
+
// AbortController guards both phases — the fetch() promise (TCP
|
|
65
|
+
// connect, headers received) AND response.text() (body streaming).
|
|
66
|
+
// Without this, a stalled upstream that holds the socket open but
|
|
67
|
+
// never sends bytes would block the entire stdio loop and every
|
|
68
|
+
// subsequent message behind it.
|
|
69
|
+
const timeoutMs = getTimeoutMs();
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
let response;
|
|
75
|
+
try {
|
|
76
|
+
response = await fetch(`${url.replace(/\/+$/, '')}/mcp`, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {
|
|
79
|
+
'Authorization': `Bearer ${token}`,
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
'Accept': 'application/json, text/event-stream',
|
|
82
|
+
},
|
|
83
|
+
body: line,
|
|
84
|
+
signal: controller.signal,
|
|
85
|
+
});
|
|
86
|
+
} catch (fetchErr) {
|
|
87
|
+
// AbortError surfaces with .name === 'AbortError' on Node 18+;
|
|
88
|
+
// distinguish it from generic transport errors so the agent
|
|
89
|
+
// can react differently (a timeout is "retry shortly", a
|
|
90
|
+
// network error is "operator's broken").
|
|
91
|
+
if (fetchErr.name === 'AbortError') {
|
|
92
|
+
return buildErrorResponse(
|
|
93
|
+
reqId,
|
|
94
|
+
-32000,
|
|
95
|
+
`Upstream /mcp timed out after ${timeoutMs}ms`,
|
|
96
|
+
{ timeoutMs }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return buildErrorResponse(
|
|
100
|
+
reqId,
|
|
101
|
+
-32000,
|
|
102
|
+
`Transport error contacting ${url}: ${fetchErr.message}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let body;
|
|
107
|
+
try {
|
|
108
|
+
body = await response.text();
|
|
109
|
+
} catch (readErr) {
|
|
110
|
+
if (readErr.name === 'AbortError') {
|
|
111
|
+
return buildErrorResponse(
|
|
112
|
+
reqId,
|
|
113
|
+
-32000,
|
|
114
|
+
`Upstream /mcp body read timed out after ${timeoutMs}ms`,
|
|
115
|
+
{ timeoutMs }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return buildErrorResponse(
|
|
119
|
+
reqId,
|
|
120
|
+
-32000,
|
|
121
|
+
`Transport error reading /mcp body: ${readErr.message}`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
return buildErrorResponse(
|
|
127
|
+
reqId,
|
|
128
|
+
-32000,
|
|
129
|
+
`Upstream /mcp returned ${response.status}`,
|
|
130
|
+
{ status: response.status, body: tryParseJson(body) ?? body }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The MCP HTTP transport (per spec) uses Content-Type to pick
|
|
135
|
+
// the response shape:
|
|
136
|
+
// - application/json — body is the raw JSON-RPC message.
|
|
137
|
+
// - text/event-stream — body is one or more SSE events; the
|
|
138
|
+
// `data:` field of each event carries the JSON-RPC payload.
|
|
139
|
+
// The dAvePi server happens to use SSE for typical responses
|
|
140
|
+
// (the SDK's StreamableHTTPServerTransport sends event: message
|
|
141
|
+
// frames), so we parse SSE first and fall back to JSON.
|
|
142
|
+
const contentType = response.headers.get('content-type') || '';
|
|
143
|
+
if (contentType.includes('text/event-stream')) {
|
|
144
|
+
const extracted = extractSseData(body);
|
|
145
|
+
if (extracted) return extracted;
|
|
146
|
+
// SSE body but no parseable data event — surface as a transport
|
|
147
|
+
// error rather than dropping the response on the floor.
|
|
148
|
+
return buildErrorResponse(
|
|
149
|
+
reqId,
|
|
150
|
+
-32000,
|
|
151
|
+
'Upstream /mcp returned SSE with no data frame',
|
|
152
|
+
{ body }
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return body;
|
|
156
|
+
} finally {
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extract the JSON-RPC payload from an SSE response body. Handles the
|
|
163
|
+
* standard cases (one `event: message` with one `data:` line) and the
|
|
164
|
+
* spec-permitted multi-line `data:` form (concatenated with newlines).
|
|
165
|
+
* Returns null when no `data:` field is present.
|
|
166
|
+
*/
|
|
167
|
+
function extractSseData(body) {
|
|
168
|
+
// Per SSE spec, events are separated by a blank line. Each event
|
|
169
|
+
// is a sequence of `field: value` lines. We only care about
|
|
170
|
+
// `data:` for forwarding the JSON-RPC payload upward.
|
|
171
|
+
const events = body.split(/\r?\n\r?\n/);
|
|
172
|
+
for (const event of events) {
|
|
173
|
+
const dataLines = [];
|
|
174
|
+
for (const line of event.split(/\r?\n/)) {
|
|
175
|
+
// Spec: `data: <value>` (the leading space is optional). Lines
|
|
176
|
+
// starting with `:` are comments; everything else is metadata
|
|
177
|
+
// we ignore for forwarding.
|
|
178
|
+
if (line.startsWith('data:')) {
|
|
179
|
+
dataLines.push(line.slice(line.startsWith('data: ') ? 6 : 5));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (dataLines.length > 0) {
|
|
183
|
+
// Multi-line data fields are joined with `\n` per spec.
|
|
184
|
+
return dataLines.join('\n');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function tryParseJson(text) {
|
|
191
|
+
try { return JSON.parse(text); } catch { return undefined; }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function runHttpProxy({ url, token }) {
|
|
195
|
+
if (!url) throw new Error('runHttpProxy: url is required');
|
|
196
|
+
if (!token) throw new Error('runHttpProxy: token is required');
|
|
197
|
+
|
|
198
|
+
const rl = readline.createInterface({
|
|
199
|
+
input: process.stdin,
|
|
200
|
+
crlfDelay: Infinity,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
for await (const line of rl) {
|
|
204
|
+
if (!line.trim()) continue;
|
|
205
|
+
const out = await forwardOne(line, { url, token });
|
|
206
|
+
process.stdout.write(out + '\n');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { runHttpProxy, forwardOne, buildErrorResponse, extractSseData };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-stdio mode: spawn `davepi mcp` from the user's local install
|
|
3
|
+
* and pipe stdio through unchanged.
|
|
4
|
+
*
|
|
5
|
+
* Why spawn vs in-process: requiring `davepi` directly would boot
|
|
6
|
+
* Mongo, Apollo, and Express in this process — a lot of side effects
|
|
7
|
+
* for a wrapper that just brokers stdio. The davepi CLI already does
|
|
8
|
+
* the right thing in its own process; we just inherit fds.
|
|
9
|
+
*
|
|
10
|
+
* Resolution order for the davepi binary:
|
|
11
|
+
* 1. ./node_modules/.bin/davepi (the project's local install)
|
|
12
|
+
* 2. PATH (a globally installed davepi)
|
|
13
|
+
*
|
|
14
|
+
* If neither resolves, surface a helpful error pointing the user at
|
|
15
|
+
* `npm install davepi` rather than a cryptic ENOENT.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const { spawn } = require('child_process');
|
|
23
|
+
|
|
24
|
+
function findDavepiBin() {
|
|
25
|
+
// 1. Project-local install. Honour the standard node_modules/.bin
|
|
26
|
+
// layout first — that's what the user gets after `npm install
|
|
27
|
+
// davepi` in their dAvePi project.
|
|
28
|
+
const local = path.join(process.cwd(), 'node_modules', '.bin', 'davepi');
|
|
29
|
+
if (fs.existsSync(local)) return local;
|
|
30
|
+
|
|
31
|
+
// Windows ships .cmd shims for the JS launcher; check that too.
|
|
32
|
+
const localCmd = path.join(process.cwd(), 'node_modules', '.bin', 'davepi.cmd');
|
|
33
|
+
if (fs.existsSync(localCmd)) return localCmd;
|
|
34
|
+
|
|
35
|
+
// 2. Fall back to PATH — a globally installed davepi.
|
|
36
|
+
return 'davepi';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runLocalStdio({ schemas } = {}) {
|
|
40
|
+
const bin = findDavepiBin();
|
|
41
|
+
const env = { ...process.env };
|
|
42
|
+
// davepi's bin reads schema/versions from the project root by
|
|
43
|
+
// default. If the user pointed somewhere non-standard via
|
|
44
|
+
// DAVEPI_SCHEMAS, propagate it.
|
|
45
|
+
if (schemas) env.DAVEPI_SCHEMAS = schemas;
|
|
46
|
+
|
|
47
|
+
// The returned Promise represents the child's lifecycle:
|
|
48
|
+
// - rejects on spawn error (ENOENT, EACCES, ...) so the bin's
|
|
49
|
+
// `main().catch()` prints + exits 1.
|
|
50
|
+
// - resolves when the child exits normally (the bin then exits
|
|
51
|
+
// 0). On non-zero exit or signal, we mirror that by exiting
|
|
52
|
+
// this process directly — bin shouldn't proceed after the
|
|
53
|
+
// subprocess has died.
|
|
54
|
+
// The previous design called `resolve()` immediately, which
|
|
55
|
+
// meant any spawn error fired AFTER the bin had already
|
|
56
|
+
// considered the work done — so `npx -y @davepi/mcp` could exit
|
|
57
|
+
// 0 even when davepi wasn't installed.
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const child = spawn(bin, ['mcp'], {
|
|
60
|
+
stdio: 'inherit',
|
|
61
|
+
env,
|
|
62
|
+
// Windows needs shell:true to pick up the .cmd shim if `bin`
|
|
63
|
+
// is a bare name. Doesn't affect Unix where `bin` is a full path.
|
|
64
|
+
shell: process.platform === 'win32',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let settled = false;
|
|
68
|
+
const settleReject = (err) => {
|
|
69
|
+
if (settled) return;
|
|
70
|
+
settled = true;
|
|
71
|
+
reject(err);
|
|
72
|
+
};
|
|
73
|
+
const settleResolve = () => {
|
|
74
|
+
if (settled) return;
|
|
75
|
+
settled = true;
|
|
76
|
+
resolve();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
child.on('error', (err) => {
|
|
80
|
+
if (err.code === 'ENOENT') {
|
|
81
|
+
process.stderr.write(
|
|
82
|
+
'davepi-mcp: could not find the `davepi` binary. ' +
|
|
83
|
+
'Install dAvePi in this project (`npm install davepi`) ' +
|
|
84
|
+
'and try again, or set DAVEPI_URL to use HTTP-proxy mode.\n'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
settleReject(err);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
child.on('exit', (code, signal) => {
|
|
91
|
+
if (signal) {
|
|
92
|
+
// Re-raise the signal in this process so a parent
|
|
93
|
+
// supervisor sees the real cause of death.
|
|
94
|
+
process.kill(process.pid, signal);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (code === 0) {
|
|
98
|
+
settleResolve();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Non-zero exit — mirror the code so CI / supervisors see
|
|
102
|
+
// the real outcome. We exit directly because resolving with
|
|
103
|
+
// a non-zero code wouldn't propagate; the bin doesn't
|
|
104
|
+
// distinguish between "child failed" and "wrapper failed".
|
|
105
|
+
process.exit(code ?? 1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Forward signals to the child so Ctrl-C from the agent cleanly
|
|
109
|
+
// terminates the davepi server.
|
|
110
|
+
const forward = (sig) => () => {
|
|
111
|
+
try { child.kill(sig); } catch (_) { /* already dead */ }
|
|
112
|
+
};
|
|
113
|
+
process.on('SIGINT', forward('SIGINT'));
|
|
114
|
+
process.on('SIGTERM', forward('SIGTERM'));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { runLocalStdio, findDavepiBin };
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@davepi/mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "One-line MCP wiring for dAvePi. Connects Claude Desktop / Cursor / Claude Code to a dAvePi instance — either a remote HTTP /mcp endpoint or a local stdio session.",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"homepage": "https://docs.davepi.dev/surfaces/mcp/",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/projik/davepi.git",
|
|
10
|
+
"directory": "packages/mcp"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"davepi",
|
|
14
|
+
"mcp",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"claude",
|
|
17
|
+
"cursor",
|
|
18
|
+
"agent"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"davepi-mcp": "bin/davepi-mcp.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin",
|
|
25
|
+
"lib",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "node --test test/*.test.js"
|
|
33
|
+
}
|
|
34
|
+
}
|