@jetpump/mcp 0.1.0-beta.1 → 0.1.0-beta.3
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/dist/index.js +36 -6
- package/dist/sentry.js +112 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,19 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// Boot order matters:
|
|
3
|
+
// 1. Sentry.init() as early as possible so unhandled errors during
|
|
4
|
+
// subsequent imports are still captured.
|
|
5
|
+
// 2. logger stderr adapter.
|
|
6
|
+
// 3. MCP server + transport.
|
|
7
|
+
import { initSentryMcp, captureException, flushSentry } from './sentry.js';
|
|
8
|
+
initSentryMcp();
|
|
2
9
|
import './logger.js';
|
|
3
10
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
12
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
6
13
|
import { tools } from './tools.js';
|
|
7
14
|
import { handleDeploy, handleListDeploys, handleTeardown } from './handlers.js';
|
|
8
|
-
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const pkg = require('../package.json');
|
|
18
|
+
const server = new Server({ name: 'jetpump', version: pkg.version }, { capabilities: { tools: {} } });
|
|
9
19
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
10
20
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
11
21
|
const { name, arguments: args } = req.params;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
try {
|
|
23
|
+
switch (name) {
|
|
24
|
+
case 'deploy': return await handleDeploy(args);
|
|
25
|
+
case 'list_deploys': return await handleListDeploys();
|
|
26
|
+
case 'teardown': return await handleTeardown(args);
|
|
27
|
+
default: throw new Error(`Unknown tool: ${name}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
// Report to Sentry with scrubbed context then rethrow so the
|
|
32
|
+
// MCP client still sees a tool error.
|
|
33
|
+
captureException(err, {
|
|
34
|
+
tool: name,
|
|
35
|
+
mcp_version: pkg.version,
|
|
36
|
+
// arguments deliberately NOT included — may contain user paths.
|
|
37
|
+
// Sentry's beforeSend scrubs anything that does make it through.
|
|
38
|
+
});
|
|
39
|
+
throw err;
|
|
17
40
|
}
|
|
18
41
|
});
|
|
42
|
+
// Best-effort flush on graceful shutdown so in-flight events reach Sentry.
|
|
43
|
+
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
44
|
+
process.on(sig, async () => {
|
|
45
|
+
await flushSentry(2000);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
19
49
|
await server.connect(new StdioServerTransport());
|
package/dist/sentry.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Sentry wiring for @jetpump/mcp — runs on the user's machine under
|
|
2
|
+
// Claude Code / Cursor / Windsurf. Every tool invocation is wrapped so
|
|
3
|
+
// any crash in deploy/list/teardown lands in the jetpump-mcp Sentry
|
|
4
|
+
// inbox with the MCP package version as the release tag.
|
|
5
|
+
//
|
|
6
|
+
// The DSN is baked into the published source because Sentry DSNs are
|
|
7
|
+
// designed to be public ingestion keys (they allow writes, not reads).
|
|
8
|
+
// Override via JETPUMP_MCP_SENTRY_DSN for testing or if you fork the
|
|
9
|
+
// package. Setting the env var to the empty string disables Sentry
|
|
10
|
+
// entirely.
|
|
11
|
+
//
|
|
12
|
+
// beforeSend strips anything that smells like a JWT or an absolute
|
|
13
|
+
// filesystem path so user source code filenames don't leak into our
|
|
14
|
+
// inbox. It's a light scrubber — a paranoid user can set
|
|
15
|
+
// JETPUMP_MCP_SENTRY_DSN="" to turn Sentry off.
|
|
16
|
+
import * as Sentry from '@sentry/node';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
const BAKED_DSN = 'https://6dcaba3d4523d78fe23bd2cd45083724@o4511219499859968.ingest.us.sentry.io/4511219510542336';
|
|
19
|
+
let initialised = false;
|
|
20
|
+
function pkgVersion() {
|
|
21
|
+
try {
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const pkg = require('../package.json');
|
|
24
|
+
return pkg.version || 'unknown';
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return 'unknown';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function scrub(obj) {
|
|
31
|
+
if (obj == null)
|
|
32
|
+
return obj;
|
|
33
|
+
if (typeof obj === 'string') {
|
|
34
|
+
// Strip bearer/JWT-ish blobs
|
|
35
|
+
return obj
|
|
36
|
+
.replace(/(Bearer\s+)[A-Za-z0-9._~\-+/=]{20,}/g, '$1<redacted>')
|
|
37
|
+
.replace(/eyJ[A-Za-z0-9._\-]{30,}/g, '<redacted-jwt>')
|
|
38
|
+
// Drop the user portion of absolute home paths
|
|
39
|
+
.replace(/\/Users\/[^/]+/g, '/Users/<user>')
|
|
40
|
+
.replace(/\/home\/[^/]+/g, '/home/<user>');
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(obj))
|
|
43
|
+
return obj.map(scrub);
|
|
44
|
+
if (typeof obj === 'object') {
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
47
|
+
if (/authorization|x-anon-token|jwt|password|secret/i.test(k)) {
|
|
48
|
+
out[k] = '<redacted>';
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
out[k] = scrub(v);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
return obj;
|
|
57
|
+
}
|
|
58
|
+
export function initSentryMcp() {
|
|
59
|
+
if (initialised)
|
|
60
|
+
return;
|
|
61
|
+
initialised = true;
|
|
62
|
+
const rawDsn = process.env.JETPUMP_MCP_SENTRY_DSN;
|
|
63
|
+
const dsn = rawDsn === undefined ? BAKED_DSN : rawDsn; // explicit empty string disables
|
|
64
|
+
if (!dsn)
|
|
65
|
+
return;
|
|
66
|
+
try {
|
|
67
|
+
Sentry.init({
|
|
68
|
+
dsn,
|
|
69
|
+
release: `@jetpump/mcp@${pkgVersion()}`,
|
|
70
|
+
environment: process.env.NODE_ENV || 'production',
|
|
71
|
+
tracesSampleRate: 0,
|
|
72
|
+
beforeSend(event) {
|
|
73
|
+
try {
|
|
74
|
+
if (event.extra)
|
|
75
|
+
event.extra = scrub(event.extra);
|
|
76
|
+
if (event.tags)
|
|
77
|
+
event.tags = scrub(event.tags);
|
|
78
|
+
if (event.breadcrumbs) {
|
|
79
|
+
event.breadcrumbs = event.breadcrumbs.map((b) => ({
|
|
80
|
+
...b,
|
|
81
|
+
data: scrub(b.data),
|
|
82
|
+
message: typeof b.message === 'string' ? scrub(b.message) : b.message,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// If scrubbing itself throws, drop the event defensively.
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return event;
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
// Never fail the MCP startup because of Sentry.
|
|
96
|
+
process.stderr.write(`[jetpump-mcp] sentry init failed: ${err.message}\n`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function captureException(err, extra = {}) {
|
|
100
|
+
try {
|
|
101
|
+
Sentry.captureException(err, { extra: scrub(extra) });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// swallow — Sentry transport is best-effort
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export async function flushSentry(timeoutMs = 2000) {
|
|
108
|
+
try {
|
|
109
|
+
await Sentry.flush(timeoutMs);
|
|
110
|
+
}
|
|
111
|
+
catch { /* ignore */ }
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetpump/mcp",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.3",
|
|
4
4
|
"description": "Jetpump MCP server — deploy the current directory from Claude Code.",
|
|
5
5
|
"homepage": "https://jetpump.dev",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
27
|
+
"@sentry/node": "^8.47.0",
|
|
27
28
|
"archiver": "^7.0.1",
|
|
28
29
|
"ignore": "^6.0.2"
|
|
29
30
|
},
|