satgate-proxy 0.1.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 +88 -0
- package/bin/satgate-proxy.js +37 -0
- package/package.json +20 -0
- package/src/bridge.js +257 -0
- package/src/index.js +78 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# satgate-proxy
|
|
2
|
+
|
|
3
|
+
**Budget-enforced MCP proxy — hard-cap your AI agent tool spend.**
|
|
4
|
+
|
|
5
|
+
MCP servers are an open tap. Every `tools/call` costs money and there's no built-in spending limit. `satgate-proxy` sits between your MCP client (Claude Desktop, Cursor) and the server, enforcing a hard budget cap via [SatGate](https://satgate.io).
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
Add to your Claude Desktop config (`claude_desktop_config.json`):
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"google-search": {
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": [
|
|
17
|
+
"satgate-proxy",
|
|
18
|
+
"--cap", "5.00",
|
|
19
|
+
"--server", "@modelcontextprotocol/server-google-search"
|
|
20
|
+
],
|
|
21
|
+
"env": {
|
|
22
|
+
"SATGATE_API_KEY": "your_macaroon_here"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Get your API key at [cloud.satgate.io](https://cloud.satgate.io).
|
|
30
|
+
|
|
31
|
+
## How It Works
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
┌──────────────┐ stdio ┌────────────────┐ SSE/HTTP ┌──────────────┐
|
|
35
|
+
│ Claude │ ──── JSON-RPC ──▶ satgate-proxy │ ──── JSON-RPC ──▶ SatGate │
|
|
36
|
+
│ Desktop │ ◀── JSON-RPC ── │ (this package) │ ◀── JSON-RPC ── │ MCP Proxy │
|
|
37
|
+
│ / Cursor │ └────────────────┘ │ + Budget │
|
|
38
|
+
└──────────────┘ │ Enforcement │
|
|
39
|
+
└──────┬───────┘
|
|
40
|
+
│
|
|
41
|
+
┌──────▼───────┐
|
|
42
|
+
│ MCP Server │
|
|
43
|
+
│ (hosted) │
|
|
44
|
+
└──────────────┘
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
1. Claude/Cursor launches `satgate-proxy` as a stdio MCP server
|
|
48
|
+
2. The proxy connects to SatGate's SSE endpoint
|
|
49
|
+
3. JSON-RPC messages from stdin are forwarded to SatGate via HTTP POST
|
|
50
|
+
4. SatGate enforces your budget cap and proxies to the real MCP server
|
|
51
|
+
5. Responses stream back via SSE and are written to stdout
|
|
52
|
+
6. If budget is exceeded → 402 → clean error back to the client
|
|
53
|
+
|
|
54
|
+
## CLI Flags
|
|
55
|
+
|
|
56
|
+
| Flag | Description | Default |
|
|
57
|
+
|------|-------------|---------|
|
|
58
|
+
| `--server <package>` | MCP server package to proxy (required) | — |
|
|
59
|
+
| `--cap <amount>` | Budget cap in USD | — |
|
|
60
|
+
| `--endpoint <url>` | SatGate proxy endpoint | `https://satgate-mcp-saas.fly.dev` |
|
|
61
|
+
| `--key <macaroon>` | API key (or use `SATGATE_API_KEY` env var) | — |
|
|
62
|
+
| `--verbose` | Debug logging to stderr | off |
|
|
63
|
+
| `-h, --help` | Show help | — |
|
|
64
|
+
|
|
65
|
+
## Why?
|
|
66
|
+
|
|
67
|
+
MCP gives AI agents direct access to paid APIs — search, code execution, databases, you name it. There's no built-in spending limit. A runaway agent can burn through hundreds of dollars in minutes.
|
|
68
|
+
|
|
69
|
+
**satgate-proxy** adds a hard cap:
|
|
70
|
+
- Set `--cap 5.00` → agent can spend at most $5
|
|
71
|
+
- Enforced server-side by SatGate (can't be bypassed by the client)
|
|
72
|
+
- Uses L402 macaroons for cryptographic budget enforcement
|
|
73
|
+
- Zero dependencies — `npx` runs it instantly
|
|
74
|
+
|
|
75
|
+
## Zero Dependencies
|
|
76
|
+
|
|
77
|
+
This package uses only Node.js built-ins. No `node_modules`, no install step. `npx satgate-proxy` just works.
|
|
78
|
+
|
|
79
|
+
## Links
|
|
80
|
+
|
|
81
|
+
- 🌐 [satgate.io](https://satgate.io) — Homepage
|
|
82
|
+
- ☁️ [cloud.satgate.io](https://cloud.satgate.io) — Dashboard & API keys
|
|
83
|
+
- 📦 [GitHub](https://github.com/SatGate-io/satgate-proxy) — Source code
|
|
84
|
+
- 📋 [MCP Specification](https://modelcontextprotocol.io) — Model Context Protocol
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { parseArgs, printUsage } = require('../src/index.js');
|
|
5
|
+
const { Bridge } = require('../src/bridge.js');
|
|
6
|
+
|
|
7
|
+
const args = parseArgs(process.argv.slice(2));
|
|
8
|
+
|
|
9
|
+
if (args.help) {
|
|
10
|
+
printUsage();
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!args.server) {
|
|
15
|
+
process.stderr.write('Error: --server <package> is required\n\n');
|
|
16
|
+
printUsage();
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const apiKey = args.key || process.env.SATGATE_API_KEY;
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
process.stderr.write('Error: API key required via --key or SATGATE_API_KEY env var\n');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const bridge = new Bridge({
|
|
27
|
+
server: args.server,
|
|
28
|
+
endpoint: args.endpoint,
|
|
29
|
+
apiKey,
|
|
30
|
+
cap: args.cap,
|
|
31
|
+
verbose: args.verbose,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
bridge.start().catch((err) => {
|
|
35
|
+
process.stderr.write(`Fatal: ${err.message}\n`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "satgate-proxy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Budget-enforced MCP proxy — hard-cap your AI agent tool spend",
|
|
5
|
+
"bin": {
|
|
6
|
+
"satgate-proxy": "./bin/satgate-proxy.js"
|
|
7
|
+
},
|
|
8
|
+
"keywords": ["mcp", "ai", "agent", "budget", "l402", "satgate", "proxy", "claude", "cursor"],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/SatGate-io/satgate-proxy"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://satgate.io",
|
|
15
|
+
"author": "SatGate Inc.",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"files": ["bin/", "src/", "README.md"]
|
|
20
|
+
}
|
package/src/bridge.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const { URL } = require('url');
|
|
6
|
+
|
|
7
|
+
class Bridge {
|
|
8
|
+
constructor({ server, endpoint, apiKey, cap, verbose }) {
|
|
9
|
+
this.server = server;
|
|
10
|
+
this.endpoint = endpoint;
|
|
11
|
+
this.apiKey = apiKey;
|
|
12
|
+
this.cap = cap;
|
|
13
|
+
this.verbose = verbose;
|
|
14
|
+
this._buffer = '';
|
|
15
|
+
this._pendingRequests = new Map();
|
|
16
|
+
this._sseBuffer = '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
log(msg) {
|
|
20
|
+
if (this.verbose) process.stderr.write(`[satgate-proxy] ${msg}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async start() {
|
|
24
|
+
this.log(`Bridging stdio ↔ ${this.endpoint}`);
|
|
25
|
+
this.log(`Server: ${this.server}`);
|
|
26
|
+
if (this.cap) this.log(`Budget cap: $${this.cap}`);
|
|
27
|
+
|
|
28
|
+
// Connect SSE session to SatGate
|
|
29
|
+
this._connectSSE();
|
|
30
|
+
|
|
31
|
+
// Read JSON-RPC from stdin and forward via HTTP POST
|
|
32
|
+
process.stdin.setEncoding('utf8');
|
|
33
|
+
process.stdin.on('data', (chunk) => this._onStdinData(chunk));
|
|
34
|
+
process.stdin.on('end', () => {
|
|
35
|
+
this.log('stdin closed, shutting down');
|
|
36
|
+
this._cleanup();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Handle stdout errors (EPIPE when reader disconnects)
|
|
40
|
+
process.stdout.on('error', (err) => {
|
|
41
|
+
if (err.code === 'EPIPE') {
|
|
42
|
+
this.log('stdout pipe closed, shutting down');
|
|
43
|
+
this._cleanup();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Graceful shutdown
|
|
48
|
+
process.on('SIGINT', () => this._cleanup());
|
|
49
|
+
process.on('SIGTERM', () => this._cleanup());
|
|
50
|
+
process.on('unhandledRejection', (err) => {
|
|
51
|
+
this.log(`Unhandled rejection: ${err.message}`);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_buildUrl(path) {
|
|
56
|
+
const base = this.endpoint.replace(/\/$/, '');
|
|
57
|
+
return `${base}${path}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_buildHeaders() {
|
|
61
|
+
const headers = {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
64
|
+
};
|
|
65
|
+
if (this.cap) {
|
|
66
|
+
headers['X-SatGate-Cap'] = this.cap;
|
|
67
|
+
}
|
|
68
|
+
headers['X-SatGate-Server'] = this.server;
|
|
69
|
+
return headers;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_connectSSE() {
|
|
73
|
+
const url = this._buildUrl('/sse');
|
|
74
|
+
this.log(`Connecting SSE: ${url}`);
|
|
75
|
+
|
|
76
|
+
const parsedUrl = new URL(url);
|
|
77
|
+
const mod = parsedUrl.protocol === 'https:' ? https : http;
|
|
78
|
+
const headers = this._buildHeaders();
|
|
79
|
+
headers['Accept'] = 'text/event-stream';
|
|
80
|
+
|
|
81
|
+
const req = mod.get(url, { headers }, (res) => {
|
|
82
|
+
if (res.statusCode !== 200) {
|
|
83
|
+
process.stderr.write(`SSE connection failed: HTTP ${res.statusCode}\n`);
|
|
84
|
+
let body = '';
|
|
85
|
+
res.on('data', (d) => body += d);
|
|
86
|
+
res.on('end', () => {
|
|
87
|
+
if (body) process.stderr.write(`Response: ${body}\n`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.log('SSE connected');
|
|
94
|
+
this._sseRes = res;
|
|
95
|
+
res.setEncoding('utf8');
|
|
96
|
+
|
|
97
|
+
res.on('data', (chunk) => this._onSSEData(chunk));
|
|
98
|
+
res.on('end', () => {
|
|
99
|
+
this.log('SSE connection closed');
|
|
100
|
+
process.exit(0);
|
|
101
|
+
});
|
|
102
|
+
res.on('error', (err) => {
|
|
103
|
+
process.stderr.write(`SSE error: ${err.message}\n`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
req.on('error', (err) => {
|
|
109
|
+
process.stderr.write(`SSE connection error: ${err.message}\n`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this._sseReq = req;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_onSSEData(chunk) {
|
|
117
|
+
this._sseBuffer += chunk;
|
|
118
|
+
const lines = this._sseBuffer.split('\n');
|
|
119
|
+
// Keep incomplete last line in buffer
|
|
120
|
+
this._sseBuffer = lines.pop() || '';
|
|
121
|
+
|
|
122
|
+
let eventType = 'message';
|
|
123
|
+
let dataLines = [];
|
|
124
|
+
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
if (line.startsWith('event:')) {
|
|
127
|
+
eventType = line.slice(6).trim();
|
|
128
|
+
} else if (line.startsWith('data:')) {
|
|
129
|
+
dataLines.push(line.slice(5).trim());
|
|
130
|
+
} else if (line === '') {
|
|
131
|
+
// End of event
|
|
132
|
+
if (dataLines.length > 0) {
|
|
133
|
+
const data = dataLines.join('\n');
|
|
134
|
+
this._handleSSEEvent(eventType, data);
|
|
135
|
+
}
|
|
136
|
+
eventType = 'message';
|
|
137
|
+
dataLines = [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_handleSSEEvent(eventType, data) {
|
|
143
|
+
this.log(`SSE event: ${eventType}`);
|
|
144
|
+
|
|
145
|
+
if (eventType === 'endpoint') {
|
|
146
|
+
// SatGate sends the POST endpoint URL via SSE
|
|
147
|
+
this._postEndpoint = data.startsWith('http') ? data : this._buildUrl(data);
|
|
148
|
+
this.log(`POST endpoint: ${this._postEndpoint}`);
|
|
149
|
+
// Flush any queued messages
|
|
150
|
+
this._flushQueue();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (eventType === 'message') {
|
|
155
|
+
this.log(`← ${data.substring(0, 200)}`);
|
|
156
|
+
// Write JSON-RPC response to stdout
|
|
157
|
+
process.stdout.write(data + '\n');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_onStdinData(chunk) {
|
|
162
|
+
this._buffer += chunk;
|
|
163
|
+
// Split on newlines — MCP uses line-delimited JSON
|
|
164
|
+
const lines = this._buffer.split('\n');
|
|
165
|
+
this._buffer = lines.pop() || '';
|
|
166
|
+
|
|
167
|
+
for (const line of lines) {
|
|
168
|
+
const trimmed = line.trim();
|
|
169
|
+
if (!trimmed) continue;
|
|
170
|
+
this._sendMessage(trimmed);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_sendMessage(jsonStr) {
|
|
175
|
+
this.log(`→ ${jsonStr.substring(0, 200)}`);
|
|
176
|
+
|
|
177
|
+
if (!this._postEndpoint) {
|
|
178
|
+
this.log('POST endpoint not yet known, queuing');
|
|
179
|
+
if (!this._queue) this._queue = [];
|
|
180
|
+
this._queue.push(jsonStr);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this._postToEndpoint(jsonStr);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_flushQueue() {
|
|
188
|
+
if (!this._queue || this._queue.length === 0) return;
|
|
189
|
+
this.log(`Flushing ${this._queue.length} queued messages`);
|
|
190
|
+
for (const msg of this._queue) {
|
|
191
|
+
this._postToEndpoint(msg);
|
|
192
|
+
}
|
|
193
|
+
this._queue = [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
_postToEndpoint(jsonStr) {
|
|
197
|
+
const url = new URL(this._postEndpoint);
|
|
198
|
+
const mod = url.protocol === 'https:' ? https : http;
|
|
199
|
+
|
|
200
|
+
const body = Buffer.from(jsonStr, 'utf8');
|
|
201
|
+
const headers = this._buildHeaders();
|
|
202
|
+
headers['Content-Length'] = body.length;
|
|
203
|
+
|
|
204
|
+
const opts = {
|
|
205
|
+
hostname: url.hostname,
|
|
206
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
207
|
+
path: url.pathname + url.search,
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const req = mod.request(opts, (res) => {
|
|
213
|
+
let respBody = '';
|
|
214
|
+
res.setEncoding('utf8');
|
|
215
|
+
res.on('data', (d) => respBody += d);
|
|
216
|
+
res.on('end', () => {
|
|
217
|
+
if (res.statusCode === 402) {
|
|
218
|
+
// Budget exceeded — synthesize JSON-RPC error
|
|
219
|
+
this.log('Budget exceeded (402)');
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(jsonStr);
|
|
222
|
+
if (parsed.id != null) {
|
|
223
|
+
const errResp = JSON.stringify({
|
|
224
|
+
jsonrpc: '2.0',
|
|
225
|
+
id: parsed.id,
|
|
226
|
+
error: {
|
|
227
|
+
code: -32000,
|
|
228
|
+
message: 'Budget exceeded. Your SatGate spending cap has been reached.',
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
process.stdout.write(errResp + '\n');
|
|
232
|
+
}
|
|
233
|
+
} catch (_) {}
|
|
234
|
+
} else if (res.statusCode >= 400) {
|
|
235
|
+
this.log(`POST error ${res.statusCode}: ${respBody}`);
|
|
236
|
+
} else {
|
|
237
|
+
this.log(`POST OK ${res.statusCode}`);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
req.on('error', (err) => {
|
|
243
|
+
process.stderr.write(`POST error: ${err.message}\n`);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
req.write(body);
|
|
247
|
+
req.end();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_cleanup() {
|
|
251
|
+
this.log('Shutting down');
|
|
252
|
+
if (this._sseReq) this._sseReq.destroy();
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = { Bridge };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_ENDPOINT = 'https://satgate-mcp-saas.fly.dev';
|
|
4
|
+
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
const args = {
|
|
7
|
+
server: null,
|
|
8
|
+
cap: null,
|
|
9
|
+
endpoint: DEFAULT_ENDPOINT,
|
|
10
|
+
key: null,
|
|
11
|
+
verbose: false,
|
|
12
|
+
help: false,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < argv.length; i++) {
|
|
16
|
+
switch (argv[i]) {
|
|
17
|
+
case '--server':
|
|
18
|
+
args.server = argv[++i];
|
|
19
|
+
break;
|
|
20
|
+
case '--cap':
|
|
21
|
+
args.cap = argv[++i];
|
|
22
|
+
break;
|
|
23
|
+
case '--endpoint':
|
|
24
|
+
args.endpoint = argv[++i];
|
|
25
|
+
break;
|
|
26
|
+
case '--key':
|
|
27
|
+
args.key = argv[++i];
|
|
28
|
+
break;
|
|
29
|
+
case '--verbose':
|
|
30
|
+
args.verbose = true;
|
|
31
|
+
break;
|
|
32
|
+
case '--help':
|
|
33
|
+
case '-h':
|
|
34
|
+
args.help = true;
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
process.stderr.write(`Unknown flag: ${argv[i]}\n`);
|
|
38
|
+
args.help = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return args;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printUsage() {
|
|
46
|
+
process.stderr.write(`satgate-proxy v0.1.0 — Budget-enforced MCP proxy
|
|
47
|
+
|
|
48
|
+
USAGE:
|
|
49
|
+
npx satgate-proxy --server <package> [options]
|
|
50
|
+
|
|
51
|
+
OPTIONS:
|
|
52
|
+
--server <package> MCP server package to proxy (required)
|
|
53
|
+
e.g. @modelcontextprotocol/server-google-search
|
|
54
|
+
--cap <amount> Budget cap in dollars (e.g. 5.00)
|
|
55
|
+
--endpoint <url> SatGate proxy endpoint
|
|
56
|
+
(default: ${DEFAULT_ENDPOINT})
|
|
57
|
+
--key <macaroon> API key / macaroon token
|
|
58
|
+
(also reads SATGATE_API_KEY env var)
|
|
59
|
+
--verbose Enable debug logging to stderr
|
|
60
|
+
-h, --help Show this help
|
|
61
|
+
|
|
62
|
+
EXAMPLE (Claude Desktop config):
|
|
63
|
+
{
|
|
64
|
+
"mcpServers": {
|
|
65
|
+
"google-search": {
|
|
66
|
+
"command": "npx",
|
|
67
|
+
"args": ["satgate-proxy", "--cap", "5.00",
|
|
68
|
+
"--server", "@modelcontextprotocol/server-google-search"],
|
|
69
|
+
"env": { "SATGATE_API_KEY": "your_macaroon_here" }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
More info: https://satgate.io
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { parseArgs, printUsage, DEFAULT_ENDPOINT };
|