@toolrelay/cli 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 +461 -0
- package/dist/api-client-7MTO2YNV.js +4 -0
- package/dist/api-client-7MTO2YNV.js.map +1 -0
- package/dist/auth-flow-VQXXGXIV.js +103 -0
- package/dist/auth-flow-VQXXGXIV.js.map +1 -0
- package/dist/chunk-3LR6JESE.js +78 -0
- package/dist/chunk-3LR6JESE.js.map +1 -0
- package/dist/chunk-7AYNBNB4.js +371 -0
- package/dist/chunk-7AYNBNB4.js.map +1 -0
- package/dist/chunk-CTTPIXB3.js +53 -0
- package/dist/chunk-CTTPIXB3.js.map +1 -0
- package/dist/credentials-KWHZKJ5O.js +4 -0
- package/dist/credentials-KWHZKJ5O.js.map +1 -0
- package/dist/index.js +2774 -0
- package/dist/index.js.map +1 -0
- package/dist/publish-RSJ4I6HJ.js +423 -0
- package/dist/publish-RSJ4I6HJ.js.map +1 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
# @toolrelay/cli
|
|
2
|
+
|
|
3
|
+
CLI for [ToolRelay](https://toolrelay.io) — define your API tools in a `toolrelay.json` config file, validate it, serve a local MCP server, and deploy to the cloud. Every tool call produces a detailed audit trace.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Interactive wizard — walks you through app and tools
|
|
9
|
+
npx @toolrelay/cli init
|
|
10
|
+
|
|
11
|
+
# Or skip prompts with a starter template
|
|
12
|
+
npx @toolrelay/cli init --yes
|
|
13
|
+
|
|
14
|
+
# Validate config
|
|
15
|
+
npx @toolrelay/cli validate toolrelay.json
|
|
16
|
+
|
|
17
|
+
# Start a local MCP server for Claude Desktop
|
|
18
|
+
npx @toolrelay/cli serve toolrelay.json
|
|
19
|
+
|
|
20
|
+
# Deploy to ToolRelay
|
|
21
|
+
npx @toolrelay/cli login
|
|
22
|
+
npx @toolrelay/cli publish toolrelay.json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Config file format (`toolrelay.json`)
|
|
26
|
+
|
|
27
|
+
The `init` wizard generates this file for you, or you can create it manually:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"app": {
|
|
32
|
+
"name": "My API",
|
|
33
|
+
"base_url": "http://localhost:3000",
|
|
34
|
+
"auth_type": "static_token",
|
|
35
|
+
"auth_config": { "token": "my-dev-key" }
|
|
36
|
+
},
|
|
37
|
+
"tools": [
|
|
38
|
+
{
|
|
39
|
+
"name": "get_user",
|
|
40
|
+
"description": "Get a user by ID",
|
|
41
|
+
"http_method": "GET",
|
|
42
|
+
"endpoint_path": "/api/users/{id}",
|
|
43
|
+
"parameter_mapping": [
|
|
44
|
+
{ "name": "id", "type": "string", "required": true, "target": "path", "description": "The user ID" }
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> **Tip:** `description` on both tools and parameters is how AI agents understand what your API does. Always include clear descriptions — they directly affect how well agents use your tools.
|
|
52
|
+
|
|
53
|
+
### Environment variable interpolation
|
|
54
|
+
|
|
55
|
+
Use `${VAR_NAME}` syntax to reference environment variables in any string field. This keeps secrets out of your config file:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"app": {
|
|
60
|
+
"base_url": "${API_BASE_URL}",
|
|
61
|
+
"auth_type": "static_token",
|
|
62
|
+
"auth_config": { "token": "${API_TOKEN}" }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
export API_BASE_URL=http://localhost:3000
|
|
69
|
+
export API_TOKEN=my-secret-key
|
|
70
|
+
npx @toolrelay/cli serve toolrelay.json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
If a referenced variable is missing, the CLI fails with a clear error listing the missing variable names. This works for `base_url`, `auth_config` values, `headers_template`, and any other string field.
|
|
74
|
+
|
|
75
|
+
### Auth types
|
|
76
|
+
|
|
77
|
+
| `auth_type` | `auth_config` fields | Description |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `none` | — | No authentication |
|
|
80
|
+
| `static_token` | `{ "token": "..." }` | Bearer token in Authorization header |
|
|
81
|
+
| `api_key_relay` | `{ "header_name": "X-API-Key" }` | Consumer API keys relayed to backend |
|
|
82
|
+
| `custom_header` | `{ "headers": { "X-Custom": "value" } }` | Custom headers with static values |
|
|
83
|
+
| `oauth2` | `{ "authorize_url", "token_url", "client_id"?, "client_secret"?, "scopes"?, "use_pkce"? }` | OAuth 2.0 authorization code flow (see [OAuth2 section](#oauth2-local-testing)) |
|
|
84
|
+
|
|
85
|
+
### Parameter mapping
|
|
86
|
+
|
|
87
|
+
Each parameter in `parameter_mapping` specifies:
|
|
88
|
+
|
|
89
|
+
| Field | Required | Description |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `name` | yes | Parameter name (what the AI agent sends) |
|
|
92
|
+
| `type` | yes | `string`, `number`, `boolean`, `object`, or `array` |
|
|
93
|
+
| `required` | yes | Whether the parameter is required |
|
|
94
|
+
| `target` | yes | Where the parameter goes: `path`, `query`, `body`, or `header` |
|
|
95
|
+
| `backend_key` | no | Backend field name if different from `name` |
|
|
96
|
+
| `default` | no | Default value when not provided |
|
|
97
|
+
| `description` | no | Human-readable description (shown to AI agents) |
|
|
98
|
+
|
|
99
|
+
#### Path parameters
|
|
100
|
+
|
|
101
|
+
Path parameters replace `{placeholder}` segments in the `endpoint_path`. The parameter `name` (or `backend_key` if set) must match the placeholder name.
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"name": "create-order",
|
|
106
|
+
"http_method": "POST",
|
|
107
|
+
"endpoint_path": "/api/customers/{customer_id}/orders",
|
|
108
|
+
"parameter_mapping": [
|
|
109
|
+
{ "name": "customer_id", "type": "string", "required": true, "target": "path" }
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Agent sends `{ "customer_id": "cust_42" }` → request goes to `/api/customers/cust_42/orders`.
|
|
115
|
+
|
|
116
|
+
#### Query parameters
|
|
117
|
+
|
|
118
|
+
Query parameters are appended as `?key=value` to the URL. Use `backend_key` when the backend expects a different name than what the agent sends, and `default` for optional filters.
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"name": "search_products",
|
|
123
|
+
"http_method": "GET",
|
|
124
|
+
"endpoint_path": "/api/products",
|
|
125
|
+
"parameter_mapping": [
|
|
126
|
+
{ "name": "query", "type": "string", "required": true, "target": "query", "backend_key": "q" },
|
|
127
|
+
{ "name": "category", "type": "string", "required": false, "target": "query" },
|
|
128
|
+
{ "name": "page", "type": "number", "required": false, "target": "query", "default": 1 },
|
|
129
|
+
{ "name": "limit", "type": "number", "required": false, "target": "query", "default": 20 }
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Agent sends `{ "query": "bluetooth speaker", "category": "electronics" }` → request goes to `/api/products?q=bluetooth+speaker&category=electronics&page=1&limit=20`.
|
|
135
|
+
|
|
136
|
+
#### Body parameters
|
|
137
|
+
|
|
138
|
+
Body parameters are sent as JSON in the request body. Use dot-notation in `backend_key` to create nested objects (e.g., `address.city` becomes `{ "address": { "city": "..." } }`).
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"name": "create_contact",
|
|
143
|
+
"http_method": "POST",
|
|
144
|
+
"endpoint_path": "/api/contacts",
|
|
145
|
+
"parameter_mapping": [
|
|
146
|
+
{ "name": "name", "type": "string", "required": true, "target": "body" },
|
|
147
|
+
{ "name": "email", "type": "string", "required": true, "target": "body" },
|
|
148
|
+
{ "name": "phone", "type": "string", "required": false, "target": "body" },
|
|
149
|
+
{ "name": "city", "type": "string", "required": false, "target": "body", "backend_key": "address.city" },
|
|
150
|
+
{ "name": "state", "type": "string", "required": false, "target": "body", "backend_key": "address.state" },
|
|
151
|
+
{ "name": "tags", "type": "array", "required": false, "target": "body", "default": [] }
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Agent sends `{ "name": "Alice", "email": "alice@example.com", "city": "Austin", "state": "TX" }` → request body becomes:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"name": "Alice",
|
|
161
|
+
"email": "alice@example.com",
|
|
162
|
+
"address": { "city": "Austin", "state": "TX" },
|
|
163
|
+
"tags": []
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### Mixing targets
|
|
168
|
+
|
|
169
|
+
A single tool can combine path, query, body, and header parameters:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"name": "update_item",
|
|
174
|
+
"http_method": "PUT",
|
|
175
|
+
"endpoint_path": "/api/items/{item_id}",
|
|
176
|
+
"parameter_mapping": [
|
|
177
|
+
{ "name": "item_id", "type": "string", "required": true, "target": "path" },
|
|
178
|
+
{ "name": "dry_run", "type": "boolean", "required": false, "target": "query", "default": false },
|
|
179
|
+
{ "name": "title", "type": "string", "required": true, "target": "body" },
|
|
180
|
+
{ "name": "price", "type": "number", "required": true, "target": "body" },
|
|
181
|
+
{ "name": "idempotency_key", "type": "string", "required": false, "target": "header", "backend_key": "Idempotency-Key" }
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Agent sends `{ "item_id": "abc", "title": "Widget", "price": 9.99, "idempotency_key": "req-1" }` → `PUT /api/items/abc?dry_run=false` with body `{ "title": "Widget", "price": 9.99 }` and header `Idempotency-Key: req-1`.
|
|
187
|
+
|
|
188
|
+
## Commands
|
|
189
|
+
|
|
190
|
+
### `init` — Generate a config file
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
npx @toolrelay/cli init # Interactive wizard
|
|
194
|
+
npx @toolrelay/cli init --yes # Starter template (no prompts)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Walks you through configuring your app (name, base URL, auth type) and defining tools (endpoints, parameters).
|
|
198
|
+
|
|
199
|
+
### `validate` — Check config
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npx @toolrelay/cli validate toolrelay.json
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Validates the config without making any HTTP calls. Catches:
|
|
206
|
+
|
|
207
|
+
- **Schema errors**: Missing required fields, invalid enum values, malformed URLs
|
|
208
|
+
- **Path parameter mismatches**: `{placeholder}` in `endpoint_path` without a matching `target: "path"` parameter (and vice versa)
|
|
209
|
+
- **Auth config**: Required fields per auth type (e.g., `token` for `static_token`, `authorize_url` for `oauth2`)
|
|
210
|
+
|
|
211
|
+
Run this in CI to catch config issues before deploying.
|
|
212
|
+
|
|
213
|
+
### `serve` — Local MCP server
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npx @toolrelay/cli serve toolrelay.json
|
|
217
|
+
npx @toolrelay/cli serve toolrelay.json --port 8787
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Starts a local MCP Streamable HTTP server that Claude Desktop (or any MCP client) can connect to. Every `tools/call` produces a full audit trace in your terminal.
|
|
221
|
+
|
|
222
|
+
| Option | Description |
|
|
223
|
+
|---|---|
|
|
224
|
+
| `--base-url <url>` | Override `base_url` from config |
|
|
225
|
+
| `-p, --port <number>` | Port to listen on (default: 8787) |
|
|
226
|
+
| `-v, --verbose` | Show response headers and extra detail |
|
|
227
|
+
|
|
228
|
+
On startup it prints the Claude Desktop config snippet:
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"mcpServers": {
|
|
233
|
+
"my-api": {
|
|
234
|
+
"url": "http://localhost:8787/mcp"
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The server implements the full MCP Streamable HTTP protocol (`initialize`, `tools/list`, `tools/call`, `ping`, SSE, session management).
|
|
241
|
+
|
|
242
|
+
**Endpoints:**
|
|
243
|
+
|
|
244
|
+
| Endpoint | Description |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `POST /mcp` | MCP JSON-RPC endpoint |
|
|
247
|
+
| `GET /mcp` | SSE stream (with `Mcp-Session-Id` header) |
|
|
248
|
+
| `GET /health` | Health check (tool count, call count, OAuth status) |
|
|
249
|
+
| `GET /timeline` | Call timeline as JSON |
|
|
250
|
+
| `GET /oauth/start` | Trigger OAuth flow (OAuth2 apps only) |
|
|
251
|
+
| `GET /oauth/status` | Check OAuth state (OAuth2 apps only) |
|
|
252
|
+
|
|
253
|
+
**Call timeline:** Every tool invocation is tracked with sequence numbers, timestamps, arguments, status, latency, and the gap between calls (how long the AI spent "thinking"). On shutdown (Ctrl+C), a formatted timeline and audit summary are printed.
|
|
254
|
+
|
|
255
|
+
**Session persistence:** Sessions are saved to `.toolrelay/sessions/` on shutdown. Browse them with `toolrelay ui`.
|
|
256
|
+
|
|
257
|
+
### `ui` — Session viewer
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
npx @toolrelay/cli ui
|
|
261
|
+
npx @toolrelay/cli ui --port 8788 --no-open
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Opens a local web UI to browse past `serve` sessions. Inspect call timelines, drill into individual tool calls to see parameter resolution, request/response bodies, diagnostics, and errors.
|
|
265
|
+
|
|
266
|
+
| Option | Description |
|
|
267
|
+
|---|---|
|
|
268
|
+
| `-p, --port <number>` | Port to listen on (default: 8788) |
|
|
269
|
+
| `--no-open` | Don't auto-open the browser |
|
|
270
|
+
|
|
271
|
+
### `login` — Authenticate with ToolRelay
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
npx @toolrelay/cli login
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Opens your browser to sign in to [toolrelay.io](https://toolrelay.io). On success, your credentials are saved to `~/.toolrelay/credentials.json` (file mode `0600`).
|
|
278
|
+
|
|
279
|
+
For CI/CD pipelines, set the `TOOLRELAY_DEPLOY_TOKEN` environment variable instead — it takes priority over the credentials file.
|
|
280
|
+
|
|
281
|
+
### `logout` — Clear saved credentials
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
npx @toolrelay/cli logout
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Removes `~/.toolrelay/credentials.json`. Does not revoke the token server-side.
|
|
288
|
+
|
|
289
|
+
### `whoami` — Show current user
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
npx @toolrelay/cli whoami
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Prints the authenticated user's email, name, and account tier. Useful for verifying which account the CLI is using.
|
|
296
|
+
|
|
297
|
+
### `publish` — Deploy to ToolRelay
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
npx @toolrelay/cli publish toolrelay.json
|
|
301
|
+
npx @toolrelay/cli publish toolrelay.json --dry-run
|
|
302
|
+
npx @toolrelay/cli publish toolrelay.json --prune
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Deploys your `toolrelay.json` config to the ToolRelay platform. The command is **idempotent** — it creates the app and tools on first run, then updates only what changed on subsequent runs.
|
|
306
|
+
|
|
307
|
+
| Option | Description |
|
|
308
|
+
|---|---|
|
|
309
|
+
| `--dry-run` | Show what would change without making any API calls |
|
|
310
|
+
| `--prune` | Delete remote tools that are no longer in the config file |
|
|
311
|
+
|
|
312
|
+
**How it works:**
|
|
313
|
+
|
|
314
|
+
1. Validates your config for production readiness (see [Pre-publish checks](#pre-publish-checks) below)
|
|
315
|
+
2. Authenticates using `TOOLRELAY_DEPLOY_TOKEN` (env var) or `~/.toolrelay/credentials.json` (from `login`)
|
|
316
|
+
3. Matches your local app to a remote app by slug (derived from `app.name`)
|
|
317
|
+
4. Creates the app if it doesn't exist, or updates it if the config changed
|
|
318
|
+
5. Syncs tools — creates new ones, updates changed ones, and optionally prunes removed ones
|
|
319
|
+
6. Auto-publishes the app if it hasn't been published yet
|
|
320
|
+
|
|
321
|
+
**What you get after publishing:**
|
|
322
|
+
|
|
323
|
+
- A hosted MCP endpoint at `https://proxy.toolrelay.io/mcp/<your-slug>` — connect any AI agent
|
|
324
|
+
- An API portal at `https://toolrelay.io/portal/<your-slug>` — consumers can request API keys
|
|
325
|
+
- Auto-generated OpenAPI spec, SKILL.md, and landing page
|
|
326
|
+
|
|
327
|
+
**Tier limits** are enforced by the API. If your plan doesn't support the number of apps or tools you're deploying, the command will fail with a clear error message.
|
|
328
|
+
|
|
329
|
+
#### Pre-publish checks
|
|
330
|
+
|
|
331
|
+
Before making any API calls, `publish` validates your config to catch common mistakes:
|
|
332
|
+
|
|
333
|
+
| Check | Severity | Description |
|
|
334
|
+
|---|---|---|
|
|
335
|
+
| Localhost / private IP | Error | `base_url` cannot point to `localhost`, `127.0.0.1`, `192.168.*`, etc. |
|
|
336
|
+
| HTTP base URL | Warning | Production APIs should use HTTPS |
|
|
337
|
+
| Missing tool descriptions | Warning | AI agents rely on descriptions to pick the right tool |
|
|
338
|
+
| OAuth2 required fields | Error | `authorize_url` and `token_url` must be set (`client_id`/`client_secret` are optional for public client PKCE) |
|
|
339
|
+
| OAuth2 localhost URLs | Error | OAuth URLs must be publicly reachable |
|
|
340
|
+
| Missing static token | Error | `static_token` auth requires a `token` in `auth_config` |
|
|
341
|
+
| Duplicate tool names | Error | Each tool must have a unique name |
|
|
342
|
+
|
|
343
|
+
Errors block the deploy. Warnings are printed but don't prevent publishing.
|
|
344
|
+
|
|
345
|
+
**Authentication:**
|
|
346
|
+
|
|
347
|
+
| Method | Use case |
|
|
348
|
+
|---|---|
|
|
349
|
+
| `toolrelay login` | Local development — opens browser, saves credentials to `~/.toolrelay/` |
|
|
350
|
+
| `TOOLRELAY_DEPLOY_TOKEN` | CI/CD — set as environment variable, takes priority over credentials file |
|
|
351
|
+
|
|
352
|
+
Generate a deploy token at **[toolrelay.io/dashboard/settings](https://toolrelay.io/dashboard/settings)** (Settings > Deploy Tokens). The token is shown once — copy it and store it as a CI secret.
|
|
353
|
+
|
|
354
|
+
**Environment variables:**
|
|
355
|
+
|
|
356
|
+
| Variable | Description |
|
|
357
|
+
|---|---|
|
|
358
|
+
| `TOOLRELAY_DEPLOY_TOKEN` | Deploy token for authentication (takes priority over credentials file) |
|
|
359
|
+
| `TOOLRELAY_API_URL` | Override the API endpoint (default: `https://api.toolrelay.io`) |
|
|
360
|
+
|
|
361
|
+
## OAuth2 local testing
|
|
362
|
+
|
|
363
|
+
When your app uses `auth_type: "oauth2"`, the `serve` command runs a full browser-based OAuth2 authorization code flow locally.
|
|
364
|
+
|
|
365
|
+
```json
|
|
366
|
+
{
|
|
367
|
+
"app": {
|
|
368
|
+
"name": "My OAuth API",
|
|
369
|
+
"base_url": "http://localhost:3000",
|
|
370
|
+
"auth_type": "oauth2",
|
|
371
|
+
"auth_config": {
|
|
372
|
+
"authorize_url": "https://provider.com/oauth/authorize",
|
|
373
|
+
"token_url": "https://provider.com/oauth/token",
|
|
374
|
+
"client_id": "${OAUTH_CLIENT_ID}",
|
|
375
|
+
"client_secret": "${OAUTH_CLIENT_SECRET}",
|
|
376
|
+
"scopes": "read write"
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
"tools": [...]
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**How it works:**
|
|
384
|
+
|
|
385
|
+
1. On startup, a temporary HTTP server starts on a random port for the OAuth callback
|
|
386
|
+
2. Your browser opens to the upstream provider's authorize URL (with PKCE by default)
|
|
387
|
+
3. After you authorize, the provider redirects to `http://localhost:<port>/callback`
|
|
388
|
+
4. The CLI exchanges the authorization code for access/refresh tokens
|
|
389
|
+
5. Tokens are stored in memory — every tool call gets `Authorization: Bearer <token>` injected automatically
|
|
390
|
+
6. If the access token expires and a refresh token is available, it's refreshed automatically
|
|
391
|
+
|
|
392
|
+
**Important:** Register `http://localhost` (any port) as an allowed redirect URI in your OAuth provider's settings.
|
|
393
|
+
|
|
394
|
+
If the browser can't auto-open (headless environments, SSH), the authorize URL is printed to the terminal. You can also manually trigger the flow at any time via `GET /oauth/start`.
|
|
395
|
+
|
|
396
|
+
### Disabling PKCE
|
|
397
|
+
|
|
398
|
+
PKCE (Proof Key for Code Exchange) is enabled by default. Some providers — like AWS Cognito configured as a confidential client, or certain custom OAuth servers — may reject the extra `code_challenge` / `code_verifier` parameters. Set `"use_pkce": false` in `auth_config` to disable it:
|
|
399
|
+
|
|
400
|
+
```json
|
|
401
|
+
{
|
|
402
|
+
"app": {
|
|
403
|
+
"name": "Cognito API",
|
|
404
|
+
"base_url": "https://api.example.com",
|
|
405
|
+
"auth_type": "oauth2",
|
|
406
|
+
"auth_config": {
|
|
407
|
+
"authorize_url": "https://mypool.auth.us-east-1.amazoncognito.com/oauth2/authorize",
|
|
408
|
+
"token_url": "https://mypool.auth.us-east-1.amazoncognito.com/oauth2/token",
|
|
409
|
+
"client_id": "${COGNITO_CLIENT_ID}",
|
|
410
|
+
"client_secret": "${COGNITO_CLIENT_SECRET}",
|
|
411
|
+
"scopes": "openid profile",
|
|
412
|
+
"use_pkce": false
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
"tools": [...]
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
| Provider | PKCE support | Recommendation |
|
|
420
|
+
|---|---|---|
|
|
421
|
+
| Auth0 | Yes | Leave default (`use_pkce: true`) |
|
|
422
|
+
| Supabase | Yes (default since 2024) | Leave default |
|
|
423
|
+
| Okta | Yes | Leave default |
|
|
424
|
+
| Google | Yes | Leave default |
|
|
425
|
+
| AWS Cognito (public client) | Yes | Leave default |
|
|
426
|
+
| AWS Cognito (confidential client) | May reject PKCE | Set `"use_pkce": false` |
|
|
427
|
+
| Custom OAuth servers | Varies | Try default first; set `false` if you get errors about unknown parameters |
|
|
428
|
+
|
|
429
|
+
## Local to production
|
|
430
|
+
|
|
431
|
+
The typical workflow:
|
|
432
|
+
|
|
433
|
+
```
|
|
434
|
+
1. toolrelay init → Generate toolrelay.json
|
|
435
|
+
2. toolrelay serve → Test locally with Claude Desktop
|
|
436
|
+
3. (iterate on config)
|
|
437
|
+
4. toolrelay login → Authenticate with toolrelay.io
|
|
438
|
+
5. toolrelay publish → Deploy to production
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Once published, your app gets:
|
|
442
|
+
|
|
443
|
+
- **Hosted MCP endpoint** (`/mcp/<slug>`) — any AI agent can connect
|
|
444
|
+
- **Consumer portal** (`/portal/<slug>`) — API key self-service for developers
|
|
445
|
+
- **Auto-generated docs** — OpenAPI spec, SKILL.md, landing page
|
|
446
|
+
- **Usage analytics** — call volume, latency, error rates per tool
|
|
447
|
+
- **Rate limiting and caching** — per-consumer, per-tier controls
|
|
448
|
+
|
|
449
|
+
Sign up at [toolrelay.io](https://toolrelay.io) to get started.
|
|
450
|
+
|
|
451
|
+
### File locations
|
|
452
|
+
|
|
453
|
+
| Path | Purpose |
|
|
454
|
+
|---|---|
|
|
455
|
+
| `toolrelay.json` | Your app + tool config (project root, committed to git) |
|
|
456
|
+
| `.toolrelay/sessions/` | Local MCP session logs (project-local, auto-gitignored) |
|
|
457
|
+
| `~/.toolrelay/credentials.json` | Login credentials (home directory, mode `0600`) |
|
|
458
|
+
|
|
459
|
+
## License
|
|
460
|
+
|
|
461
|
+
Proprietary — see LICENSE for details.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"api-client-7MTO2YNV.js"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { getApiUrl, saveCredentials } from './chunk-CTTPIXB3.js';
|
|
3
|
+
import { ToolRelayApiClient } from './chunk-3LR6JESE.js';
|
|
4
|
+
import { createServer } from 'http';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
|
|
8
|
+
var BOLD = "\x1B[1m";
|
|
9
|
+
var DIM = "\x1B[2m";
|
|
10
|
+
var RESET = "\x1B[0m";
|
|
11
|
+
var GREEN = "\x1B[32m";
|
|
12
|
+
function openBrowser(url) {
|
|
13
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? 'start ""' : "xdg-open";
|
|
14
|
+
exec(`${cmd} "${url}"`, (err) => {
|
|
15
|
+
if (err) {
|
|
16
|
+
console.log(`
|
|
17
|
+
${DIM}Could not open browser automatically.${RESET}`);
|
|
18
|
+
console.log(`${DIM}Open this URL in your browser:${RESET}
|
|
19
|
+
`);
|
|
20
|
+
console.log(` ${url}
|
|
21
|
+
`);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async function loginFlow() {
|
|
26
|
+
const apiUrl = getApiUrl();
|
|
27
|
+
const webUrl = process.env.TOOLRELAY_WEB_URL || apiUrl.replace(/\/\/api\./, "//");
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
30
|
+
let server;
|
|
31
|
+
const timeout = setTimeout(() => {
|
|
32
|
+
server?.close();
|
|
33
|
+
reject(new Error("Login timed out after 5 minutes. Try again."));
|
|
34
|
+
}, 5 * 60 * 1e3);
|
|
35
|
+
server = createServer((req, res) => {
|
|
36
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
37
|
+
if (url.pathname === "/callback") {
|
|
38
|
+
const token = url.searchParams.get("token");
|
|
39
|
+
const returnedState = url.searchParams.get("state");
|
|
40
|
+
const error = url.searchParams.get("error");
|
|
41
|
+
if (error) {
|
|
42
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
43
|
+
res.end("<html><body><h2>Login failed</h2><p>You can close this tab.</p></body></html>");
|
|
44
|
+
clearTimeout(timeout);
|
|
45
|
+
server.close();
|
|
46
|
+
reject(new Error(`Login failed: ${error}`));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!token || returnedState !== state) {
|
|
50
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
51
|
+
res.end("<html><body><h2>Invalid callback</h2><p>Missing token or state mismatch.</p></body></html>");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
55
|
+
res.end("<html><body><h2>Logged in!</h2><p>You can close this tab and return to your terminal.</p></body></html>");
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
server.close();
|
|
58
|
+
verifyAndSave(token, apiUrl).then(resolve).catch(reject);
|
|
59
|
+
} else {
|
|
60
|
+
res.writeHead(404);
|
|
61
|
+
res.end();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
server.listen(0, "127.0.0.1", () => {
|
|
65
|
+
const port = server.address().port;
|
|
66
|
+
const callbackUrl = `http://localhost:${port}/callback`;
|
|
67
|
+
const loginUrl = `${webUrl}/cli-auth?callback=${encodeURIComponent(callbackUrl)}&state=${state}`;
|
|
68
|
+
console.log(`
|
|
69
|
+
${BOLD}Opening browser to log in...${RESET}
|
|
70
|
+
`);
|
|
71
|
+
openBrowser(loginUrl);
|
|
72
|
+
if (!process.stdout.isTTY) {
|
|
73
|
+
console.log(`${DIM}Open this URL to log in:${RESET}`);
|
|
74
|
+
console.log(` ${loginUrl}
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
console.log(`${DIM}Waiting for authentication...${RESET}`);
|
|
78
|
+
});
|
|
79
|
+
server.on("error", (err) => {
|
|
80
|
+
clearTimeout(timeout);
|
|
81
|
+
reject(err);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function verifyAndSave(token, apiUrl) {
|
|
86
|
+
const client = new ToolRelayApiClient({ token, api_url: apiUrl, source: "env" });
|
|
87
|
+
const user = await client.whoami();
|
|
88
|
+
saveCredentials({
|
|
89
|
+
token,
|
|
90
|
+
email: user.email,
|
|
91
|
+
api_url: apiUrl,
|
|
92
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
93
|
+
});
|
|
94
|
+
console.log(`
|
|
95
|
+
${GREEN}\u2713${RESET} Logged in as ${BOLD}${user.email}${RESET} (${user.tier} tier)`);
|
|
96
|
+
console.log(`${DIM}Credentials saved to ~/.toolrelay/credentials.json${RESET}
|
|
97
|
+
`);
|
|
98
|
+
return { email: user.email, token };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { loginFlow };
|
|
102
|
+
//# sourceMappingURL=auth-flow-VQXXGXIV.js.map
|
|
103
|
+
//# sourceMappingURL=auth-flow-VQXXGXIV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/auth-flow.ts"],"names":[],"mappings":"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","file":"auth-flow-VQXXGXIV.js","sourcesContent":["import { createServer } from 'node:http';\nimport type { Server } from 'node:http';\nimport crypto from 'node:crypto';\nimport { exec } from 'node:child_process';\nimport { saveCredentials, getApiUrl } from './credentials.js';\nimport { ToolRelayApiClient } from './api-client.js';\n\n// ANSI helpers\nconst BOLD = '\\x1b[1m';\nconst DIM = '\\x1b[2m';\nconst RESET = '\\x1b[0m';\nconst GREEN = '\\x1b[32m';\nconst RED = '\\x1b[31m';\n\n// ─── Browser opener (reused from oauth-handler) ─────────────────────────────\n\nfunction openBrowser(url: string): void {\n const cmd =\n process.platform === 'darwin'\n ? 'open'\n : process.platform === 'win32'\n ? 'start \"\"'\n : 'xdg-open';\n\n exec(`${cmd} \"${url}\"`, (err) => {\n if (err) {\n console.log(`\\n${DIM}Could not open browser automatically.${RESET}`);\n console.log(`${DIM}Open this URL in your browser:${RESET}\\n`);\n console.log(` ${url}\\n`);\n }\n });\n}\n\n// ─── Login flow ─────────────────────────────────────────────────────────────\n\n/**\n * Starts a local HTTP server, opens the ToolRelay login page in the browser,\n * and waits for the callback with a JWT token.\n *\n * The web dashboard redirects to: http://localhost:<port>/callback?token=<jwt>\n */\nexport async function loginFlow(): Promise<{ email: string; token: string }> {\n const apiUrl = getApiUrl();\n // Derive the web (frontend) URL by stripping the \"api.\" subdomain.\n // TOOLRELAY_WEB_URL is a dev-only override for local development\n // where the API runs on a plain localhost port.\n const webUrl = process.env.TOOLRELAY_WEB_URL\n || apiUrl.replace(/\\/\\/api\\./, '//');\n\n return new Promise((resolve, reject) => {\n const state = crypto.randomBytes(16).toString('hex');\n let server: Server;\n\n const timeout = setTimeout(() => {\n server?.close();\n reject(new Error('Login timed out after 5 minutes. Try again.'));\n }, 5 * 60 * 1000);\n\n server = createServer((req, res) => {\n const url = new URL(req.url ?? '/', `http://localhost`);\n\n if (url.pathname === '/callback') {\n const token = url.searchParams.get('token');\n const returnedState = url.searchParams.get('state');\n const error = url.searchParams.get('error');\n\n if (error) {\n res.writeHead(200, { 'Content-Type': 'text/html' });\n res.end('<html><body><h2>Login failed</h2><p>You can close this tab.</p></body></html>');\n clearTimeout(timeout);\n server.close();\n reject(new Error(`Login failed: ${error}`));\n return;\n }\n\n if (!token || returnedState !== state) {\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end('<html><body><h2>Invalid callback</h2><p>Missing token or state mismatch.</p></body></html>');\n return;\n }\n\n res.writeHead(200, { 'Content-Type': 'text/html' });\n res.end('<html><body><h2>Logged in!</h2><p>You can close this tab and return to your terminal.</p></body></html>');\n\n clearTimeout(timeout);\n server.close();\n\n // Verify the token works and get user info\n verifyAndSave(token, apiUrl)\n .then(resolve)\n .catch(reject);\n } else {\n res.writeHead(404);\n res.end();\n }\n });\n\n server.listen(0, '127.0.0.1', () => {\n const port = (server.address() as { port: number }).port;\n const callbackUrl = `http://localhost:${port}/callback`;\n const loginUrl = `${webUrl}/cli-auth?callback=${encodeURIComponent(callbackUrl)}&state=${state}`;\n\n console.log(`\\n${BOLD}Opening browser to log in...${RESET}\\n`);\n openBrowser(loginUrl);\n\n if (!process.stdout.isTTY) {\n console.log(`${DIM}Open this URL to log in:${RESET}`);\n console.log(` ${loginUrl}\\n`);\n }\n\n console.log(`${DIM}Waiting for authentication...${RESET}`);\n });\n\n server.on('error', (err) => {\n clearTimeout(timeout);\n reject(err);\n });\n });\n}\n\nasync function verifyAndSave(token: string, apiUrl: string): Promise<{ email: string; token: string }> {\n const client = new ToolRelayApiClient({ token, api_url: apiUrl, source: 'env' });\n\n const user = await client.whoami();\n\n saveCredentials({\n token,\n email: user.email,\n api_url: apiUrl,\n created_at: new Date().toISOString(),\n });\n\n console.log(`\\n${GREEN}✓${RESET} Logged in as ${BOLD}${user.email}${RESET} (${user.tier} tier)`);\n console.log(`${DIM}Credentials saved to ~/.toolrelay/credentials.json${RESET}\\n`);\n\n return { email: user.email, token };\n}\n"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/api-client.ts
|
|
3
|
+
var ToolRelayApiClient = class {
|
|
4
|
+
baseUrl;
|
|
5
|
+
token;
|
|
6
|
+
constructor(auth) {
|
|
7
|
+
this.baseUrl = auth.api_url.replace(/\/$/, "");
|
|
8
|
+
this.token = auth.token;
|
|
9
|
+
}
|
|
10
|
+
// ─── Auth ───────────────────────────────────────────────────────────
|
|
11
|
+
async whoami() {
|
|
12
|
+
return this.request("GET", "/api/auth/me");
|
|
13
|
+
}
|
|
14
|
+
// ─── Apps ───────────────────────────────────────────────────────────
|
|
15
|
+
async listApps() {
|
|
16
|
+
return this.request("GET", "/api/apps");
|
|
17
|
+
}
|
|
18
|
+
async getApp(appId) {
|
|
19
|
+
return this.request("GET", `/api/apps/${appId}`);
|
|
20
|
+
}
|
|
21
|
+
async createApp(body) {
|
|
22
|
+
return this.request("POST", "/api/apps", body);
|
|
23
|
+
}
|
|
24
|
+
async updateApp(appId, body) {
|
|
25
|
+
return this.request("PUT", `/api/apps/${appId}`, body);
|
|
26
|
+
}
|
|
27
|
+
async publishApp(appId) {
|
|
28
|
+
return this.request("POST", `/api/apps/${appId}/publish`);
|
|
29
|
+
}
|
|
30
|
+
// ─── Tools ──────────────────────────────────────────────────────────
|
|
31
|
+
async listTools(appId) {
|
|
32
|
+
return this.request("GET", `/api/apps/${appId}/tools`);
|
|
33
|
+
}
|
|
34
|
+
async createTool(appId, body) {
|
|
35
|
+
return this.request("POST", `/api/apps/${appId}/tools`, body);
|
|
36
|
+
}
|
|
37
|
+
async updateTool(appId, toolId, body) {
|
|
38
|
+
return this.request("PUT", `/api/apps/${appId}/tools/${toolId}`, body);
|
|
39
|
+
}
|
|
40
|
+
async deleteTool(appId, toolId) {
|
|
41
|
+
await this.request("DELETE", `/api/apps/${appId}/tools/${toolId}`);
|
|
42
|
+
}
|
|
43
|
+
// ─── HTTP ───────────────────────────────────────────────────────────
|
|
44
|
+
async request(method, path, body) {
|
|
45
|
+
const url = `${this.baseUrl}${path}`;
|
|
46
|
+
const headers = {
|
|
47
|
+
"Authorization": `Bearer ${this.token}`,
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
"User-Agent": "toolrelay-cli/0.1.0"
|
|
50
|
+
};
|
|
51
|
+
const response = await fetch(url, {
|
|
52
|
+
method,
|
|
53
|
+
headers,
|
|
54
|
+
body: body ? JSON.stringify(body) : void 0
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
let errorMessage;
|
|
58
|
+
try {
|
|
59
|
+
const errorBody = await response.json();
|
|
60
|
+
errorMessage = errorBody.error || errorBody.message || response.statusText;
|
|
61
|
+
} catch {
|
|
62
|
+
errorMessage = response.statusText;
|
|
63
|
+
}
|
|
64
|
+
const err = {
|
|
65
|
+
status: response.status,
|
|
66
|
+
message: errorMessage
|
|
67
|
+
};
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
const text = await response.text();
|
|
71
|
+
if (!text) return {};
|
|
72
|
+
return JSON.parse(text);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export { ToolRelayApiClient };
|
|
77
|
+
//# sourceMappingURL=chunk-3LR6JESE.js.map
|
|
78
|
+
//# sourceMappingURL=chunk-3LR6JESE.js.map
|