caplets 0.5.1 → 0.6.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 +41 -0
- package/caplets/context7.md +33 -0
- package/caplets/github/CAPLET.md +54 -0
- package/caplets/github/README.md +13 -0
- package/caplets/linear/CAPLET.md +50 -0
- package/caplets/linear/workflows.md +9 -0
- package/dist/index.js +480 -266
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -129,6 +129,15 @@ CAPLETS_CONFIG=/path/to/config.json caplets init
|
|
|
129
129
|
CAPLETS_CONFIG=/path/to/config.json caplets serve
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
+
Inspect the installed CLI version and resolved config locations:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
caplets --version
|
|
136
|
+
caplets config path
|
|
137
|
+
caplets config paths
|
|
138
|
+
caplets config paths --json
|
|
139
|
+
```
|
|
140
|
+
|
|
132
141
|
Caplets validates this file at startup. Config changes take effect after restarting the
|
|
133
142
|
Caplets MCP server.
|
|
134
143
|
|
|
@@ -201,6 +210,30 @@ Top-level files derive their Caplet ID from the filename. Directory-style Caplet
|
|
|
201
210
|
`linear/CAPLET.md`, which is exposed as `linear`; sibling files can be referenced with
|
|
202
211
|
normal Markdown links from `CAPLET.md`.
|
|
203
212
|
|
|
213
|
+
This repository includes polished working examples under [`caplets/`](caplets/):
|
|
214
|
+
|
|
215
|
+
- `github`: GitHub's official MCP server container, using `GITHUB_PERSONAL_ACCESS_TOKEN`.
|
|
216
|
+
- `linear`: Linear's hosted OAuth MCP endpoint.
|
|
217
|
+
- `context7`: Context7 documentation lookup through `@upstash/context7-mcp`.
|
|
218
|
+
|
|
219
|
+
Install every example from a repo's `caplets/` directory:
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
caplets install spiritledsoftware/caplets
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Install one or more individual Caplets by ID:
|
|
226
|
+
|
|
227
|
+
```sh
|
|
228
|
+
caplets install spiritledsoftware/caplets github
|
|
229
|
+
caplets install spiritledsoftware/caplets github linear
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`caplets install` accepts a GitHub `owner/repo` shorthand, a Git URL, or a local repository path.
|
|
233
|
+
It installs into your user Caplets root, which is `~/.caplets` by default or the parent directory
|
|
234
|
+
of `CAPLETS_CONFIG` when that environment variable is set. Existing Caplets are not overwritten
|
|
235
|
+
unless `--force` is passed.
|
|
236
|
+
|
|
204
237
|
Caplets always loads user Caplet files from `~/.caplets`. Project `./.caplets/config.json`
|
|
205
238
|
is still loaded as project config, but project Markdown Caplet files are executable
|
|
206
239
|
configuration and are ignored unless explicitly trusted:
|
|
@@ -390,6 +423,14 @@ caplets auth list
|
|
|
390
423
|
caplets auth logout <server>
|
|
391
424
|
```
|
|
392
425
|
|
|
426
|
+
To list configured Caplets without starting downstream backends:
|
|
427
|
+
|
|
428
|
+
```sh
|
|
429
|
+
caplets list
|
|
430
|
+
caplets list --all
|
|
431
|
+
caplets list --json
|
|
432
|
+
```
|
|
433
|
+
|
|
393
434
|
### Optional Server Settings
|
|
394
435
|
|
|
395
436
|
Every server can set:
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
$schema: https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json
|
|
3
|
+
name: Context7 Documentation
|
|
4
|
+
description: Fetch current library and framework documentation through Context7 before using version-sensitive APIs.
|
|
5
|
+
tags:
|
|
6
|
+
- docs
|
|
7
|
+
- libraries
|
|
8
|
+
- frameworks
|
|
9
|
+
- api-reference
|
|
10
|
+
mcpServer:
|
|
11
|
+
command: npx
|
|
12
|
+
args:
|
|
13
|
+
- -y
|
|
14
|
+
- "@upstash/context7-mcp"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Context7 Documentation
|
|
18
|
+
|
|
19
|
+
Use this Caplet when the agent needs up-to-date library, SDK, framework, CLI, or cloud-service
|
|
20
|
+
documentation before writing code or giving technical instructions.
|
|
21
|
+
|
|
22
|
+
## Good Fits
|
|
23
|
+
|
|
24
|
+
- Check current API signatures for fast-moving JavaScript, TypeScript, Python, or cloud libraries.
|
|
25
|
+
- Look up migration notes before changing framework configuration.
|
|
26
|
+
- Retrieve official examples for a specific package version.
|
|
27
|
+
- Resolve uncertainty about CLI flags, config files, or SDK initialization.
|
|
28
|
+
|
|
29
|
+
## Use Carefully
|
|
30
|
+
|
|
31
|
+
- Prefer primary documentation over snippets when implementation risk is high.
|
|
32
|
+
- Record the library or package name clearly before searching.
|
|
33
|
+
- Do not use this as a substitute for project-local types and tests.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
$schema: https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json
|
|
3
|
+
name: GitHub
|
|
4
|
+
description: Inspect and manage GitHub repositories, issues, pull requests, branches, commits, and code review workflows.
|
|
5
|
+
tags:
|
|
6
|
+
- code
|
|
7
|
+
- github
|
|
8
|
+
- pull-requests
|
|
9
|
+
- issues
|
|
10
|
+
- reviews
|
|
11
|
+
mcpServer:
|
|
12
|
+
command: docker
|
|
13
|
+
args:
|
|
14
|
+
- run
|
|
15
|
+
- -i
|
|
16
|
+
- --rm
|
|
17
|
+
- -e
|
|
18
|
+
- GITHUB_PERSONAL_ACCESS_TOKEN
|
|
19
|
+
- ghcr.io/github/github-mcp-server
|
|
20
|
+
env:
|
|
21
|
+
GITHUB_PERSONAL_ACCESS_TOKEN: $env:GITHUB_PERSONAL_ACCESS_TOKEN
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# GitHub
|
|
25
|
+
|
|
26
|
+
Use this Caplet when the agent needs live GitHub repository context or needs to act on
|
|
27
|
+
issues, pull requests, branches, commits, or review feedback.
|
|
28
|
+
|
|
29
|
+
## Good Fits
|
|
30
|
+
|
|
31
|
+
- Summarize recent pull request activity before a code review.
|
|
32
|
+
- Inspect open issues and identify implementation work.
|
|
33
|
+
- Create or update issues from an implementation plan.
|
|
34
|
+
- Compare branches, inspect commits, or review pull request files.
|
|
35
|
+
- Leave review comments after the agent has inspected the relevant diff.
|
|
36
|
+
|
|
37
|
+
## Use Carefully
|
|
38
|
+
|
|
39
|
+
- Mutating operations can affect real repositories. Prefer read operations first.
|
|
40
|
+
- Use a least-privilege `GITHUB_PERSONAL_ACCESS_TOKEN`.
|
|
41
|
+
- Do not ask the agent to expose token values, repository secrets, or private issue contents outside
|
|
42
|
+
the intended conversation.
|
|
43
|
+
|
|
44
|
+
## Setup
|
|
45
|
+
|
|
46
|
+
Create a GitHub personal access token with the minimum repository scopes needed for your workflow,
|
|
47
|
+
then export it before starting Caplets:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
export GITHUB_PERSONAL_ACCESS_TOKEN=github_pat_...
|
|
51
|
+
caplets serve
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This Caplet uses GitHub's official MCP server container, so Docker must be available on the host.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# GitHub Caplet
|
|
2
|
+
|
|
3
|
+
This Caplet wraps GitHub's official MCP server container:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN ghcr.io/github/github-mcp-server
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Install it from this repo:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
caplets install spiritledsoftware/caplets github
|
|
13
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
$schema: https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json
|
|
3
|
+
name: Linear
|
|
4
|
+
description: Plan and track product work in Linear by reading teams, projects, cycles, issues, comments, and workflow state.
|
|
5
|
+
tags:
|
|
6
|
+
- planning
|
|
7
|
+
- linear
|
|
8
|
+
- issues
|
|
9
|
+
- projects
|
|
10
|
+
- triage
|
|
11
|
+
mcpServer:
|
|
12
|
+
transport: http
|
|
13
|
+
url: https://mcp.linear.app/mcp
|
|
14
|
+
auth:
|
|
15
|
+
type: oauth2
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Linear
|
|
19
|
+
|
|
20
|
+
Use this Caplet when the agent needs live product planning context from Linear or needs to keep
|
|
21
|
+
implementation work synchronized with issues, projects, and team workflows.
|
|
22
|
+
|
|
23
|
+
## Good Fits
|
|
24
|
+
|
|
25
|
+
- Find the current issue or project that matches a requested feature.
|
|
26
|
+
- Summarize open work by team, project, cycle, label, or assignee.
|
|
27
|
+
- Draft issue breakdowns from a technical plan.
|
|
28
|
+
- Add implementation notes or status comments after code changes.
|
|
29
|
+
- Check whether a bug or feature already has active work before creating a new issue.
|
|
30
|
+
|
|
31
|
+
## Reference Files
|
|
32
|
+
|
|
33
|
+
- [Workflows](./workflows.md): recommended lookup, planning, status update, and triage flows.
|
|
34
|
+
|
|
35
|
+
## Use Carefully
|
|
36
|
+
|
|
37
|
+
- Linear issue updates are visible to teammates. Read first, then write deliberately.
|
|
38
|
+
- Keep issue titles and comments concise; use links to detailed implementation artifacts when useful.
|
|
39
|
+
- Avoid broad, noisy searches when a team key, issue ID, project, or label is available.
|
|
40
|
+
|
|
41
|
+
## Setup
|
|
42
|
+
|
|
43
|
+
Authenticate once through Caplets:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
caplets auth login linear
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The Linear MCP endpoint supports OAuth. Caplets stores the resulting token bundle in your local
|
|
50
|
+
Caplets auth store.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Linear Workflows
|
|
2
|
+
|
|
3
|
+
Useful agent flows:
|
|
4
|
+
|
|
5
|
+
- **Issue lookup**: search by issue key first, then by title or project if no key is provided.
|
|
6
|
+
- **Planning**: read project context, summarize constraints, then create child issues only when the
|
|
7
|
+
requested breakdown is clear.
|
|
8
|
+
- **Status updates**: comment with what changed, verification run, and remaining risk.
|
|
9
|
+
- **Triage**: group candidate issues by urgency, owner, and whether they are blocked.
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import minproc, { stdin, stdout, default as process$1 } from "node:process";
|
|
4
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
-
import {
|
|
6
|
-
import minpath, { basename, dirname, extname, isAbsolute, join } from "node:path";
|
|
4
|
+
import { accessSync, chmodSync, constants, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import minpath, { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
6
|
import { fileURLToPath as urlToPath } from "node:url";
|
|
7
|
+
import { homedir, tmpdir } from "node:os";
|
|
8
8
|
import { PassThrough } from "node:stream";
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
10
|
import { createHash, randomBytes } from "node:crypto";
|
|
11
|
+
import { execFileSync } from "node:child_process";
|
|
11
12
|
import { createInterface } from "node:readline/promises";
|
|
12
13
|
//#region \0rolldown/runtime.js
|
|
13
14
|
var __create = Object.create;
|
|
@@ -19848,7 +19849,7 @@ const EMPTY_COMPLETION_RESULT = { completion: {
|
|
|
19848
19849
|
} };
|
|
19849
19850
|
//#endregion
|
|
19850
19851
|
//#region package.json
|
|
19851
|
-
var version = "0.
|
|
19852
|
+
var version = "0.6.0";
|
|
19852
19853
|
//#endregion
|
|
19853
19854
|
//#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
19854
19855
|
/**
|
|
@@ -27413,6 +27414,42 @@ function matter(file, options) {
|
|
|
27413
27414
|
} else file.data.matter = {};
|
|
27414
27415
|
}
|
|
27415
27416
|
//#endregion
|
|
27417
|
+
//#region src/config/validation.ts
|
|
27418
|
+
const SERVER_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
27419
|
+
const HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
27420
|
+
const FORBIDDEN_HEADERS = new Set([
|
|
27421
|
+
"accept",
|
|
27422
|
+
"authorization",
|
|
27423
|
+
"connection",
|
|
27424
|
+
"content-length",
|
|
27425
|
+
"content-type",
|
|
27426
|
+
"host",
|
|
27427
|
+
"keep-alive",
|
|
27428
|
+
"mcp-protocol-version",
|
|
27429
|
+
"mcp-session-id",
|
|
27430
|
+
"proxy-authenticate",
|
|
27431
|
+
"proxy-authorization",
|
|
27432
|
+
"te",
|
|
27433
|
+
"trailer",
|
|
27434
|
+
"transfer-encoding",
|
|
27435
|
+
"upgrade"
|
|
27436
|
+
]);
|
|
27437
|
+
function isAllowedRemoteUrl(value) {
|
|
27438
|
+
let url;
|
|
27439
|
+
try {
|
|
27440
|
+
url = new URL(value);
|
|
27441
|
+
} catch {
|
|
27442
|
+
return false;
|
|
27443
|
+
}
|
|
27444
|
+
if (url.protocol === "https:") return true;
|
|
27445
|
+
return url.protocol === "http:" && [
|
|
27446
|
+
"localhost",
|
|
27447
|
+
"127.0.0.1",
|
|
27448
|
+
"[::1]",
|
|
27449
|
+
"::1"
|
|
27450
|
+
].includes(url.hostname);
|
|
27451
|
+
}
|
|
27452
|
+
//#endregion
|
|
27416
27453
|
//#region src/errors.ts
|
|
27417
27454
|
var CapletsError = class extends Error {
|
|
27418
27455
|
code;
|
|
@@ -27465,25 +27502,6 @@ function errorResult(error, fallback) {
|
|
|
27465
27502
|
//#region src/caplet-files.ts
|
|
27466
27503
|
const MAX_CAPLET_FILE_BYTES = 128 * 1024;
|
|
27467
27504
|
const MAX_CAPLET_BODY_CHARS = 64 * 1024;
|
|
27468
|
-
const SERVER_ID_PATTERN$1 = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
27469
|
-
const HEADER_NAME_PATTERN$1 = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
27470
|
-
const FORBIDDEN_HEADERS$1 = new Set([
|
|
27471
|
-
"accept",
|
|
27472
|
-
"authorization",
|
|
27473
|
-
"connection",
|
|
27474
|
-
"content-length",
|
|
27475
|
-
"content-type",
|
|
27476
|
-
"host",
|
|
27477
|
-
"keep-alive",
|
|
27478
|
-
"mcp-protocol-version",
|
|
27479
|
-
"mcp-session-id",
|
|
27480
|
-
"proxy-authenticate",
|
|
27481
|
-
"proxy-authorization",
|
|
27482
|
-
"te",
|
|
27483
|
-
"trailer",
|
|
27484
|
-
"transfer-encoding",
|
|
27485
|
-
"upgrade"
|
|
27486
|
-
]);
|
|
27487
27505
|
const capletRemoteAuthSchema = discriminatedUnion("type", [
|
|
27488
27506
|
object({ type: literal("none") }).strict(),
|
|
27489
27507
|
object({
|
|
@@ -27590,7 +27608,7 @@ const capletMcpServerSchema = object({
|
|
|
27590
27608
|
path: ["url"],
|
|
27591
27609
|
message: "remote servers require url"
|
|
27592
27610
|
});
|
|
27593
|
-
if (server.url && !hasEnvReference$1(server.url) && !isAllowedRemoteUrl
|
|
27611
|
+
if (server.url && !hasEnvReference$1(server.url) && !isAllowedRemoteUrl(server.url)) ctx.addIssue({
|
|
27594
27612
|
code: "custom",
|
|
27595
27613
|
path: ["url"],
|
|
27596
27614
|
message: "remote url must use https except loopback development urls"
|
|
@@ -27610,7 +27628,7 @@ const capletMcpServerSchema = object({
|
|
|
27610
27628
|
}
|
|
27611
27629
|
if (server.auth?.type === "headers") for (const headerName of Object.keys(server.auth.headers)) {
|
|
27612
27630
|
const normalized = headerName.toLowerCase();
|
|
27613
|
-
if (!HEADER_NAME_PATTERN
|
|
27631
|
+
if (!HEADER_NAME_PATTERN.test(headerName) || FORBIDDEN_HEADERS.has(normalized)) ctx.addIssue({
|
|
27614
27632
|
code: "custom",
|
|
27615
27633
|
path: [
|
|
27616
27634
|
"auth",
|
|
@@ -27634,12 +27652,12 @@ const capletOpenApiEndpointSchema = object({
|
|
|
27634
27652
|
code: "custom",
|
|
27635
27653
|
message: "openapiEndpoint must define exactly one spec source: specPath or specUrl"
|
|
27636
27654
|
});
|
|
27637
|
-
if (endpoint.specUrl && !hasEnvReference$1(endpoint.specUrl) && !isAllowedRemoteUrl
|
|
27655
|
+
if (endpoint.specUrl && !hasEnvReference$1(endpoint.specUrl) && !isAllowedRemoteUrl(endpoint.specUrl)) ctx.addIssue({
|
|
27638
27656
|
code: "custom",
|
|
27639
27657
|
path: ["specUrl"],
|
|
27640
27658
|
message: "OpenAPI specUrl must use https except loopback development urls"
|
|
27641
27659
|
});
|
|
27642
|
-
if (endpoint.baseUrl && !hasEnvReference$1(endpoint.baseUrl) && !isAllowedRemoteUrl
|
|
27660
|
+
if (endpoint.baseUrl && !hasEnvReference$1(endpoint.baseUrl) && !isAllowedRemoteUrl(endpoint.baseUrl)) ctx.addIssue({
|
|
27643
27661
|
code: "custom",
|
|
27644
27662
|
path: ["baseUrl"],
|
|
27645
27663
|
message: "OpenAPI baseUrl must use https except loopback development urls"
|
|
@@ -27662,7 +27680,7 @@ const capletGraphQlEndpointSchema = object({
|
|
|
27662
27680
|
schemaPath: string().min(1).optional().describe("Local GraphQL SDL or introspection path."),
|
|
27663
27681
|
schemaUrl: string().min(1).optional().describe("Remote GraphQL SDL or introspection URL."),
|
|
27664
27682
|
introspection: literal(true).optional().describe("Load schema through endpoint introspection."),
|
|
27665
|
-
operations: record(string().regex(SERVER_ID_PATTERN
|
|
27683
|
+
operations: record(string().regex(SERVER_ID_PATTERN), capletGraphQlOperationSchema).optional().describe("Configured GraphQL operations keyed by stable tool name."),
|
|
27666
27684
|
auth: capletEndpointAuthSchema.describe("Explicit GraphQL request auth config. Use {\"type\":\"none\"} for public APIs."),
|
|
27667
27685
|
requestTimeoutMs: number$1().int().positive().optional().describe("Timeout in milliseconds for GraphQL HTTP requests."),
|
|
27668
27686
|
operationCacheTtlMs: number$1().int().nonnegative().optional().describe("Milliseconds GraphQL operation metadata stays fresh. Set 0 to refresh every time."),
|
|
@@ -27673,12 +27691,12 @@ const capletGraphQlEndpointSchema = object({
|
|
|
27673
27691
|
code: "custom",
|
|
27674
27692
|
message: "graphqlEndpoint must define exactly one schema source: schemaPath, schemaUrl, or introspection"
|
|
27675
27693
|
});
|
|
27676
|
-
if (endpoint.endpointUrl && !hasEnvReference$1(endpoint.endpointUrl) && !isAllowedRemoteUrl
|
|
27694
|
+
if (endpoint.endpointUrl && !hasEnvReference$1(endpoint.endpointUrl) && !isAllowedRemoteUrl(endpoint.endpointUrl)) ctx.addIssue({
|
|
27677
27695
|
code: "custom",
|
|
27678
27696
|
path: ["endpointUrl"],
|
|
27679
27697
|
message: "GraphQL endpointUrl must use https except loopback development urls"
|
|
27680
27698
|
});
|
|
27681
|
-
if (endpoint.schemaUrl && !hasEnvReference$1(endpoint.schemaUrl) && !isAllowedRemoteUrl
|
|
27699
|
+
if (endpoint.schemaUrl && !hasEnvReference$1(endpoint.schemaUrl) && !isAllowedRemoteUrl(endpoint.schemaUrl)) ctx.addIssue({
|
|
27682
27700
|
code: "custom",
|
|
27683
27701
|
path: ["schemaUrl"],
|
|
27684
27702
|
message: "GraphQL schemaUrl must use https except loopback development urls"
|
|
@@ -27756,6 +27774,9 @@ function readCapletFile(path) {
|
|
|
27756
27774
|
if (!parsed.success) throw new CapletsError("CONFIG_INVALID", `Caplet file at ${path} has invalid frontmatter`, parsed.error.issues);
|
|
27757
27775
|
return capletToServerConfig(parsed.data, body, dirname(path));
|
|
27758
27776
|
}
|
|
27777
|
+
function validateCapletFile(path) {
|
|
27778
|
+
readCapletFile(path);
|
|
27779
|
+
}
|
|
27759
27780
|
function capletToServerConfig(frontmatter, body, baseDir) {
|
|
27760
27781
|
if (frontmatter.openapiEndpoint) return {
|
|
27761
27782
|
...frontmatter.openapiEndpoint,
|
|
@@ -27799,7 +27820,7 @@ function validateEndpointAuthHeaders$1(auth, ctx) {
|
|
|
27799
27820
|
if (auth?.type !== "headers") return;
|
|
27800
27821
|
for (const headerName of Object.keys(auth.headers)) {
|
|
27801
27822
|
const normalized = headerName.toLowerCase();
|
|
27802
|
-
if (!HEADER_NAME_PATTERN
|
|
27823
|
+
if (!HEADER_NAME_PATTERN.test(headerName) || FORBIDDEN_HEADERS.has(normalized)) ctx.addIssue({
|
|
27803
27824
|
code: "custom",
|
|
27804
27825
|
path: [
|
|
27805
27826
|
"auth",
|
|
@@ -27831,7 +27852,7 @@ function isPlainObject$2(value) {
|
|
|
27831
27852
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
27832
27853
|
}
|
|
27833
27854
|
function validateCapletId(id, path) {
|
|
27834
|
-
if (!SERVER_ID_PATTERN
|
|
27855
|
+
if (!SERVER_ID_PATTERN.test(id)) throw new CapletsError("CONFIG_INVALID", `Caplet file at ${path} derives invalid ID ${id}; ID must match ^[a-zA-Z0-9_-]{1,64}$`);
|
|
27835
27856
|
}
|
|
27836
27857
|
function hasEnvReference$1(value) {
|
|
27837
27858
|
return /\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$env:[A-Za-z_][A-Za-z0-9_]*/.test(value);
|
|
@@ -27844,48 +27865,35 @@ function isUrl(value) {
|
|
|
27844
27865
|
return false;
|
|
27845
27866
|
}
|
|
27846
27867
|
}
|
|
27847
|
-
|
|
27848
|
-
|
|
27849
|
-
|
|
27850
|
-
|
|
27851
|
-
|
|
27852
|
-
|
|
27853
|
-
|
|
27854
|
-
|
|
27855
|
-
|
|
27856
|
-
|
|
27868
|
+
//#endregion
|
|
27869
|
+
//#region src/config/paths.ts
|
|
27870
|
+
const DEFAULT_CONFIG_PATH = join(homedir(), ".caplets", "config.json");
|
|
27871
|
+
const DEFAULT_AUTH_DIR = join(homedir(), ".caplets", "auth");
|
|
27872
|
+
const PROJECT_CONFIG_FILE = join(".caplets", "config.json");
|
|
27873
|
+
const TRUST_PROJECT_CAPLETS_ENV = "CAPLETS_TRUST_PROJECT_CAPLETS";
|
|
27874
|
+
function resolveConfigPath(path) {
|
|
27875
|
+
return path ?? DEFAULT_CONFIG_PATH;
|
|
27876
|
+
}
|
|
27877
|
+
function resolveProjectConfigPath(cwd = process.cwd()) {
|
|
27878
|
+
return join(cwd, PROJECT_CONFIG_FILE);
|
|
27879
|
+
}
|
|
27880
|
+
function resolveCapletsRoot(configPath = resolveConfigPath()) {
|
|
27881
|
+
return dirname(configPath);
|
|
27882
|
+
}
|
|
27883
|
+
function resolveProjectCapletsRoot(cwd = process.cwd()) {
|
|
27884
|
+
return join(cwd, ".caplets");
|
|
27885
|
+
}
|
|
27886
|
+
function isTrustedEnvEnabled(value) {
|
|
27887
|
+
return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
|
|
27857
27888
|
}
|
|
27858
27889
|
//#endregion
|
|
27859
27890
|
//#region src/config.ts
|
|
27860
|
-
const SERVER_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
27861
|
-
const HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
27862
|
-
const FORBIDDEN_HEADERS = new Set([
|
|
27863
|
-
"accept",
|
|
27864
|
-
"authorization",
|
|
27865
|
-
"connection",
|
|
27866
|
-
"content-length",
|
|
27867
|
-
"content-type",
|
|
27868
|
-
"host",
|
|
27869
|
-
"keep-alive",
|
|
27870
|
-
"mcp-protocol-version",
|
|
27871
|
-
"mcp-session-id",
|
|
27872
|
-
"proxy-authenticate",
|
|
27873
|
-
"proxy-authorization",
|
|
27874
|
-
"te",
|
|
27875
|
-
"trailer",
|
|
27876
|
-
"transfer-encoding",
|
|
27877
|
-
"upgrade"
|
|
27878
|
-
]);
|
|
27879
27891
|
const NON_INTERPOLATED_SERVER_FIELDS = new Set([
|
|
27880
27892
|
"name",
|
|
27881
27893
|
"description",
|
|
27882
27894
|
"tags",
|
|
27883
27895
|
"body"
|
|
27884
27896
|
]);
|
|
27885
|
-
join(homedir(), ".caplets", "config.json");
|
|
27886
|
-
join(homedir(), ".caplets", "auth");
|
|
27887
|
-
const PROJECT_CONFIG_FILE = join(".caplets", "config.json");
|
|
27888
|
-
const TRUST_PROJECT_CAPLETS_ENV = "CAPLETS_TRUST_PROJECT_CAPLETS";
|
|
27889
27897
|
const remoteAuthSchema = discriminatedUnion("type", [
|
|
27890
27898
|
object({ type: literal("none") }).strict(),
|
|
27891
27899
|
object({
|
|
@@ -28192,15 +28200,6 @@ function configSchemaFor(serverValueSchema, openApiEndpointValueSchema, graphQlE
|
|
|
28192
28200
|
}
|
|
28193
28201
|
const configFileSchema = configSchemaFor(publicServerSchema, publicOpenApiEndpointSchema, publicGraphQlEndpointSchema);
|
|
28194
28202
|
const normalizedConfigFileSchema = configSchemaFor(normalizedServerSchema, normalizedOpenApiEndpointSchema, normalizedGraphQlEndpointSchema);
|
|
28195
|
-
function resolveConfigPath(path) {
|
|
28196
|
-
return path ?? join(homedir(), ".caplets", "config.json");
|
|
28197
|
-
}
|
|
28198
|
-
function resolveProjectConfigPath(cwd = process.cwd()) {
|
|
28199
|
-
return join(cwd, PROJECT_CONFIG_FILE);
|
|
28200
|
-
}
|
|
28201
|
-
function resolveCapletsRoot(configPath = resolveConfigPath()) {
|
|
28202
|
-
return dirname(configPath);
|
|
28203
|
-
}
|
|
28204
28203
|
function loadConfig(path = resolveConfigPath(), projectPath = resolveProjectConfigPath()) {
|
|
28205
28204
|
const hasUserConfig = existsSync(path);
|
|
28206
28205
|
const hasProjectConfig = existsSync(projectPath);
|
|
@@ -28221,9 +28220,6 @@ function loadConfig(path = resolveConfigPath(), projectPath = resolveProjectConf
|
|
|
28221
28220
|
function shouldLoadProjectCaplets() {
|
|
28222
28221
|
return isTrustedEnvEnabled(process.env[TRUST_PROJECT_CAPLETS_ENV]);
|
|
28223
28222
|
}
|
|
28224
|
-
function isTrustedEnvEnabled(value) {
|
|
28225
|
-
return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
|
|
28226
|
-
}
|
|
28227
28223
|
function readPublicConfigInput(path) {
|
|
28228
28224
|
try {
|
|
28229
28225
|
const normalized = normalizeLocalPaths(JSON.parse(readFileSync(path, "utf8")), dirname(path));
|
|
@@ -28369,17 +28365,6 @@ function hasEnvReference(value) {
|
|
|
28369
28365
|
function interpolateEnv(value) {
|
|
28370
28366
|
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => process.env[name] ?? "").replace(/\$env:([A-Za-z_][A-Za-z0-9_]*)/g, (_match, name) => process.env[name] ?? "");
|
|
28371
28367
|
}
|
|
28372
|
-
function isAllowedRemoteUrl(value) {
|
|
28373
|
-
const url = new URL(value);
|
|
28374
|
-
if (url.protocol === "https:") return true;
|
|
28375
|
-
if (url.protocol !== "http:") return false;
|
|
28376
|
-
return [
|
|
28377
|
-
"localhost",
|
|
28378
|
-
"127.0.0.1",
|
|
28379
|
-
"[::1]",
|
|
28380
|
-
"::1"
|
|
28381
|
-
].includes(url.hostname);
|
|
28382
|
-
}
|
|
28383
28368
|
//#endregion
|
|
28384
28369
|
//#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/client.js
|
|
28385
28370
|
/**
|
|
@@ -31574,14 +31559,22 @@ var SSEClientTransport = class {
|
|
|
31574
31559
|
}
|
|
31575
31560
|
};
|
|
31576
31561
|
//#endregion
|
|
31577
|
-
//#region src/auth.ts
|
|
31562
|
+
//#region src/auth/store.ts
|
|
31578
31563
|
function authStorePath(server, authDir = join(homedir(), ".caplets", "auth")) {
|
|
31579
|
-
|
|
31564
|
+
if (!server || server.includes("/") || server.includes("\\") || server.includes("..")) throw new CapletsError("REQUEST_INVALID", `Invalid auth store server name ${server}`);
|
|
31565
|
+
const authRoot = resolve(authDir);
|
|
31566
|
+
const candidate = resolve(authRoot, `${server}.json`);
|
|
31567
|
+
if (candidate !== authRoot && candidate.startsWith(`${authRoot}${sep}`)) return candidate;
|
|
31568
|
+
throw new CapletsError("REQUEST_INVALID", `Invalid auth store server name ${server}`);
|
|
31580
31569
|
}
|
|
31581
31570
|
function readTokenBundle(server, authDir) {
|
|
31582
31571
|
const path = authStorePath(server, authDir);
|
|
31583
31572
|
if (!existsSync(path)) return;
|
|
31584
|
-
|
|
31573
|
+
try {
|
|
31574
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
31575
|
+
} catch {
|
|
31576
|
+
return;
|
|
31577
|
+
}
|
|
31585
31578
|
}
|
|
31586
31579
|
function deleteTokenBundle(server, authDir) {
|
|
31587
31580
|
const path = authStorePath(server, authDir);
|
|
@@ -31605,6 +31598,8 @@ function writeTokenBundle(bundle, authDir) {
|
|
|
31605
31598
|
} catch {}
|
|
31606
31599
|
renameSync(tempPath, path);
|
|
31607
31600
|
}
|
|
31601
|
+
//#endregion
|
|
31602
|
+
//#region src/auth.ts
|
|
31608
31603
|
function staticRemoteHeaders(server) {
|
|
31609
31604
|
if (server.auth?.type === "bearer") return { authorization: `Bearer ${server.auth.token}` };
|
|
31610
31605
|
if (server.auth?.type === "headers") return server.auth.headers;
|
|
@@ -31714,8 +31709,11 @@ var FileOAuthProvider = class {
|
|
|
31714
31709
|
}
|
|
31715
31710
|
addClientAuthentication = async (headers, params) => {
|
|
31716
31711
|
if (this.server.auth?.type !== "oauth2" && this.server.auth?.type !== "oidc") return;
|
|
31717
|
-
|
|
31718
|
-
|
|
31712
|
+
const clientInformation = this.clientInformation();
|
|
31713
|
+
const clientId = clientInformation?.client_id;
|
|
31714
|
+
const clientSecret = clientInformation?.client_secret;
|
|
31715
|
+
if (clientId) params.set("client_id", clientId);
|
|
31716
|
+
if (clientSecret) params.set("client_secret", clientSecret);
|
|
31719
31717
|
headers.set("content-type", "application/x-www-form-urlencoded");
|
|
31720
31718
|
};
|
|
31721
31719
|
};
|
|
@@ -45575,7 +45573,7 @@ var require_utilities = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
45575
45573
|
var _resolveSchemaCoordinate = require_resolveSchemaCoordinate();
|
|
45576
45574
|
}));
|
|
45577
45575
|
//#endregion
|
|
45578
|
-
//#region src/
|
|
45576
|
+
//#region src/http/utils.ts
|
|
45579
45577
|
var import_graphql = (/* @__PURE__ */ __commonJSMin(((exports) => {
|
|
45580
45578
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45581
45579
|
Object.defineProperty(exports, "BREAK", {
|
|
@@ -46877,7 +46875,40 @@ var import_graphql = (/* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
46877
46875
|
var _index5 = require_error$1();
|
|
46878
46876
|
var _index6 = require_utilities();
|
|
46879
46877
|
})))();
|
|
46880
|
-
|
|
46878
|
+
function parseHttpBody(contentType, text) {
|
|
46879
|
+
if (!text) return;
|
|
46880
|
+
const mime = contentType.split(";")[0]?.toLowerCase().trim() ?? "";
|
|
46881
|
+
if (mime !== "application/json" && !mime.endsWith("+json") && !mime.endsWith("/json")) return text;
|
|
46882
|
+
try {
|
|
46883
|
+
return JSON.parse(text);
|
|
46884
|
+
} catch {
|
|
46885
|
+
return text;
|
|
46886
|
+
}
|
|
46887
|
+
}
|
|
46888
|
+
async function readLimitedText(response, options) {
|
|
46889
|
+
if (!response.body) return "";
|
|
46890
|
+
const reader = response.body.getReader();
|
|
46891
|
+
const chunks = [];
|
|
46892
|
+
let bytes = 0;
|
|
46893
|
+
while (true) {
|
|
46894
|
+
const { done, value } = await reader.read();
|
|
46895
|
+
if (done) break;
|
|
46896
|
+
if (value) {
|
|
46897
|
+
bytes += value.byteLength;
|
|
46898
|
+
if (bytes > (options.maxBytes ?? 1048576)) {
|
|
46899
|
+
await reader.cancel();
|
|
46900
|
+
throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", options.errorMessage);
|
|
46901
|
+
}
|
|
46902
|
+
chunks.push(value);
|
|
46903
|
+
}
|
|
46904
|
+
}
|
|
46905
|
+
return new TextDecoder().decode(Buffer.concat(chunks));
|
|
46906
|
+
}
|
|
46907
|
+
function isAbortError(error) {
|
|
46908
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
46909
|
+
}
|
|
46910
|
+
//#endregion
|
|
46911
|
+
//#region src/graphql.ts
|
|
46881
46912
|
const GRAPHQL_METHOD = "POST";
|
|
46882
46913
|
const SCALAR_JSON_SCHEMA = {
|
|
46883
46914
|
String: { type: "string" },
|
|
@@ -46963,7 +46994,7 @@ var GraphQLManager = class {
|
|
|
46963
46994
|
challenge: response.headers.get("www-authenticate") ? "[REDACTED]" : void 0,
|
|
46964
46995
|
...endpoint.auth.type === "oauth2" || endpoint.auth.type === "oidc" ? { nextAction: "run_caplets_auth_login" } : {}
|
|
46965
46996
|
});
|
|
46966
|
-
const body =
|
|
46997
|
+
const body = parseHttpBody(response.headers.get("content-type") ?? "", await readGraphQlText(response));
|
|
46967
46998
|
const result = {
|
|
46968
46999
|
status: response.status,
|
|
46969
47000
|
statusText: response.statusText,
|
|
@@ -46979,7 +47010,7 @@ var GraphQLManager = class {
|
|
|
46979
47010
|
isError: !response.ok || Boolean(body && typeof body === "object" && "errors" in body && body.errors)
|
|
46980
47011
|
};
|
|
46981
47012
|
} catch (error) {
|
|
46982
|
-
if (isAbortError
|
|
47013
|
+
if (isAbortError(error)) throw new CapletsError("TOOL_CALL_TIMEOUT", `GraphQL request timed out for ${endpoint.server}/${toolName}`);
|
|
46983
47014
|
if (error instanceof CapletsError) throw error;
|
|
46984
47015
|
throw new CapletsError("DOWNSTREAM_TOOL_ERROR", `GraphQL request failed for ${endpoint.server}/${toolName}`, toSafeError(error));
|
|
46985
47016
|
} finally {
|
|
@@ -47055,7 +47086,7 @@ async function loadSchema(endpoint, authDir) {
|
|
|
47055
47086
|
}
|
|
47056
47087
|
const response = await postGraphQl(endpoint, endpoint.endpointUrl, { query: (0, import_graphql.getIntrospectionQuery)() }, authDir);
|
|
47057
47088
|
if (!response.ok) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "GraphQL introspection request failed", { status: response.status });
|
|
47058
|
-
const parsed = JSON.parse(await
|
|
47089
|
+
const parsed = JSON.parse(await readGraphQlText(response));
|
|
47059
47090
|
if (parsed.errors || !parsed.data) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "GraphQL introspection returned errors");
|
|
47060
47091
|
return (0, import_graphql.buildClientSchema)(parsed.data);
|
|
47061
47092
|
}
|
|
@@ -47237,7 +47268,7 @@ async function postGraphQl(endpoint, url, payload, authDir) {
|
|
|
47237
47268
|
body: JSON.stringify(payload)
|
|
47238
47269
|
});
|
|
47239
47270
|
} catch (error) {
|
|
47240
|
-
if (isAbortError
|
|
47271
|
+
if (isAbortError(error)) throw new CapletsError("TOOL_CALL_TIMEOUT", "GraphQL request timed out");
|
|
47241
47272
|
throw error;
|
|
47242
47273
|
} finally {
|
|
47243
47274
|
clearTimeout(timeout);
|
|
@@ -47254,14 +47285,14 @@ async function fetchGraphQlText(endpoint, url, authDir, sendAuth = true) {
|
|
|
47254
47285
|
headers: { ...sendAuth ? schemaAuthHeaders(endpoint, authDir) : {} }
|
|
47255
47286
|
});
|
|
47256
47287
|
} catch (error) {
|
|
47257
|
-
if (isAbortError
|
|
47288
|
+
if (isAbortError(error)) throw new CapletsError("TOOL_CALL_TIMEOUT", "GraphQL schema request timed out");
|
|
47258
47289
|
throw error;
|
|
47259
47290
|
} finally {
|
|
47260
47291
|
clearTimeout(timeout);
|
|
47261
47292
|
}
|
|
47262
47293
|
if (response.status >= 300 && response.status < 400) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "GraphQL schema request returned a redirect");
|
|
47263
47294
|
if (!response.ok) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "GraphQL schema request failed", { status: response.status });
|
|
47264
|
-
return
|
|
47295
|
+
return readGraphQlText(response);
|
|
47265
47296
|
}
|
|
47266
47297
|
function schemaAuthHeaders(endpoint, authDir) {
|
|
47267
47298
|
return {
|
|
@@ -47283,48 +47314,13 @@ function staticHeaders(endpoint) {
|
|
|
47283
47314
|
function shouldSendSchemaAuth(endpoint) {
|
|
47284
47315
|
return Boolean(endpoint.schemaUrl && new URL(endpoint.schemaUrl).origin === new URL(endpoint.endpointUrl).origin);
|
|
47285
47316
|
}
|
|
47286
|
-
function
|
|
47287
|
-
|
|
47288
|
-
if (!contentType.includes("application/json")) return text;
|
|
47289
|
-
try {
|
|
47290
|
-
return JSON.parse(text);
|
|
47291
|
-
} catch {
|
|
47292
|
-
return text;
|
|
47293
|
-
}
|
|
47294
|
-
}
|
|
47295
|
-
async function readLimitedText$1(response) {
|
|
47296
|
-
if (!response.body) return "";
|
|
47297
|
-
const reader = response.body.getReader();
|
|
47298
|
-
const chunks = [];
|
|
47299
|
-
let bytes = 0;
|
|
47300
|
-
while (true) {
|
|
47301
|
-
const { done, value } = await reader.read();
|
|
47302
|
-
if (done) break;
|
|
47303
|
-
if (value) {
|
|
47304
|
-
bytes += value.byteLength;
|
|
47305
|
-
if (bytes > MAX_RESPONSE_BYTES$1) {
|
|
47306
|
-
await reader.cancel();
|
|
47307
|
-
throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "GraphQL response exceeded byte limit");
|
|
47308
|
-
}
|
|
47309
|
-
chunks.push(value);
|
|
47310
|
-
}
|
|
47311
|
-
}
|
|
47312
|
-
return new TextDecoder().decode(Buffer.concat(chunks));
|
|
47317
|
+
async function readGraphQlText(response) {
|
|
47318
|
+
return readLimitedText(response, { errorMessage: "GraphQL response exceeded byte limit" });
|
|
47313
47319
|
}
|
|
47314
47320
|
function validateEndpointUrl(value) {
|
|
47315
|
-
|
|
47316
|
-
if (url.protocol === "https:") return;
|
|
47317
|
-
if (url.protocol === "http:" && [
|
|
47318
|
-
"localhost",
|
|
47319
|
-
"127.0.0.1",
|
|
47320
|
-
"[::1]",
|
|
47321
|
-
"::1"
|
|
47322
|
-
].includes(url.hostname)) return;
|
|
47321
|
+
if (isAllowedRemoteUrl(value)) return;
|
|
47323
47322
|
throw new CapletsError("CONFIG_INVALID", "GraphQL URLs must use https except loopback development urls");
|
|
47324
47323
|
}
|
|
47325
|
-
function isAbortError$1(error) {
|
|
47326
|
-
return error instanceof DOMException && error.name === "AbortError";
|
|
47327
|
-
}
|
|
47328
47324
|
function graphQlCacheKey(endpoint) {
|
|
47329
47325
|
return JSON.stringify({
|
|
47330
47326
|
endpointUrl: endpoint.endpointUrl,
|
|
@@ -56838,7 +56834,6 @@ const HTTP_METHODS = [
|
|
|
56838
56834
|
"trace"
|
|
56839
56835
|
];
|
|
56840
56836
|
const JSON_CONTENT_TYPES = ["application/json"];
|
|
56841
|
-
const MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
56842
56837
|
const FORBIDDEN_ARGUMENT_HEADERS = new Set([
|
|
56843
56838
|
"accept",
|
|
56844
56839
|
"authorization",
|
|
@@ -57159,7 +57154,7 @@ function shouldSendSpecAuth(endpoint) {
|
|
|
57159
57154
|
}
|
|
57160
57155
|
async function readResponse(response) {
|
|
57161
57156
|
const contentType = response.headers.get("content-type") ?? "";
|
|
57162
|
-
const body =
|
|
57157
|
+
const body = parseHttpBody(contentType, await readLimitedText(response, { errorMessage: "OpenAPI response exceeded byte limit" }));
|
|
57163
57158
|
return {
|
|
57164
57159
|
status: response.status,
|
|
57165
57160
|
statusText: response.statusText,
|
|
@@ -57167,34 +57162,6 @@ async function readResponse(response) {
|
|
|
57167
57162
|
...body === void 0 ? {} : { body }
|
|
57168
57163
|
};
|
|
57169
57164
|
}
|
|
57170
|
-
function parseResponseBody(contentType, text) {
|
|
57171
|
-
if (!text) return;
|
|
57172
|
-
if (!contentType.includes("application/json")) return text;
|
|
57173
|
-
try {
|
|
57174
|
-
return JSON.parse(text);
|
|
57175
|
-
} catch {
|
|
57176
|
-
return text;
|
|
57177
|
-
}
|
|
57178
|
-
}
|
|
57179
|
-
async function readLimitedText(response) {
|
|
57180
|
-
if (!response.body) return "";
|
|
57181
|
-
const reader = response.body.getReader();
|
|
57182
|
-
const chunks = [];
|
|
57183
|
-
let bytes = 0;
|
|
57184
|
-
while (true) {
|
|
57185
|
-
const { done, value } = await reader.read();
|
|
57186
|
-
if (done) break;
|
|
57187
|
-
if (value) {
|
|
57188
|
-
bytes += value.byteLength;
|
|
57189
|
-
if (bytes > MAX_RESPONSE_BYTES) {
|
|
57190
|
-
await reader.cancel();
|
|
57191
|
-
throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "OpenAPI response exceeded byte limit");
|
|
57192
|
-
}
|
|
57193
|
-
chunks.push(value);
|
|
57194
|
-
}
|
|
57195
|
-
}
|
|
57196
|
-
return new TextDecoder().decode(Buffer.concat(chunks));
|
|
57197
|
-
}
|
|
57198
57165
|
async function fetchWithLimit(url, timeoutMs, headers = {}) {
|
|
57199
57166
|
const controller = new AbortController();
|
|
57200
57167
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -57206,7 +57173,7 @@ async function fetchWithLimit(url, timeoutMs, headers = {}) {
|
|
|
57206
57173
|
});
|
|
57207
57174
|
if (response.status >= 300 && response.status < 400) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "OpenAPI spec request returned a redirect");
|
|
57208
57175
|
if (!response.ok) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "OpenAPI spec request failed", { status: response.status });
|
|
57209
|
-
return await readLimitedText(response);
|
|
57176
|
+
return await readLimitedText(response, { errorMessage: "OpenAPI response exceeded byte limit" });
|
|
57210
57177
|
} catch (error) {
|
|
57211
57178
|
if (isAbortError(error)) throw new CapletsError("TOOL_CALL_TIMEOUT", "OpenAPI spec request timed out");
|
|
57212
57179
|
throw error;
|
|
@@ -57217,7 +57184,7 @@ async function fetchWithLimit(url, timeoutMs, headers = {}) {
|
|
|
57217
57184
|
function validateOperationBaseUrl(endpoint, base) {
|
|
57218
57185
|
if (!base) throw new CapletsError("CONFIG_INVALID", `${endpoint.server} is missing OpenAPI baseUrl`);
|
|
57219
57186
|
if (endpoint.specUrl && !endpoint.baseUrl) throw new CapletsError("CONFIG_INVALID", `${endpoint.server} must configure baseUrl when using remote specUrl`);
|
|
57220
|
-
if (!
|
|
57187
|
+
if (!isAllowedRemoteUrl(base)) throw new CapletsError("CONFIG_INVALID", `${endpoint.server} OpenAPI baseUrl is not allowed`);
|
|
57221
57188
|
const url = new URL(base);
|
|
57222
57189
|
if (url.username || url.password || url.search || url.hash) throw new CapletsError("CONFIG_INVALID", `${endpoint.server} OpenAPI baseUrl must not include credentials, query, or fragment`);
|
|
57223
57190
|
}
|
|
@@ -57227,20 +57194,6 @@ function buildOperationUrl(base, operationPath) {
|
|
|
57227
57194
|
baseUrl.pathname = [baseUrl.pathname.replace(/\/+$/, ""), operationPath.replace(/^\/+/, "")].filter(Boolean).join("/");
|
|
57228
57195
|
return baseUrl;
|
|
57229
57196
|
}
|
|
57230
|
-
function isAbortError(error) {
|
|
57231
|
-
return error instanceof DOMException && error.name === "AbortError";
|
|
57232
|
-
}
|
|
57233
|
-
function isAllowedRequestBaseUrl(value) {
|
|
57234
|
-
const url = new URL(value);
|
|
57235
|
-
if (url.protocol === "https:") return true;
|
|
57236
|
-
if (url.protocol !== "http:") return false;
|
|
57237
|
-
return [
|
|
57238
|
-
"localhost",
|
|
57239
|
-
"127.0.0.1",
|
|
57240
|
-
"[::1]",
|
|
57241
|
-
"::1"
|
|
57242
|
-
].includes(url.hostname);
|
|
57243
|
-
}
|
|
57244
57197
|
function openApiCacheKey(endpoint) {
|
|
57245
57198
|
return JSON.stringify({
|
|
57246
57199
|
specPath: endpoint.specPath,
|
|
@@ -60522,75 +60475,9 @@ const { program, createCommand, createArgument, createOption, CommanderError, In
|
|
|
60522
60475
|
exports.InvalidOptionArgumentError = InvalidArgumentError;
|
|
60523
60476
|
})))(), 1)).default;
|
|
60524
60477
|
//#endregion
|
|
60525
|
-
//#region src/cli.ts
|
|
60526
|
-
async function runCli(args, io = {}) {
|
|
60527
|
-
const program = createProgram(io);
|
|
60528
|
-
try {
|
|
60529
|
-
await program.parseAsync([
|
|
60530
|
-
"node",
|
|
60531
|
-
"caplets",
|
|
60532
|
-
...args
|
|
60533
|
-
]);
|
|
60534
|
-
} catch (error) {
|
|
60535
|
-
if (error instanceof CommanderError) {
|
|
60536
|
-
if (error.code === "commander.helpDisplayed" || error.code === "commander.version") return;
|
|
60537
|
-
throw new CapletsError("REQUEST_INVALID", error.message);
|
|
60538
|
-
}
|
|
60539
|
-
throw error;
|
|
60540
|
-
}
|
|
60541
|
-
}
|
|
60542
|
-
function createProgram(io = {}) {
|
|
60543
|
-
const writeOut = io.writeOut ?? ((value) => process.stdout.write(value));
|
|
60544
|
-
const writeErr = io.writeErr ?? ((value) => process.stderr.write(value));
|
|
60545
|
-
const program = new Command();
|
|
60546
|
-
program.name("caplets").description("Progressive-disclosure gateway for MCP servers.").exitOverride().configureOutput({
|
|
60547
|
-
writeOut,
|
|
60548
|
-
writeErr,
|
|
60549
|
-
outputError: (value, write) => write(value)
|
|
60550
|
-
});
|
|
60551
|
-
program.command("init").description("Create a starter Caplets config file.").option("--force", "overwrite an existing config file").action((options) => {
|
|
60552
|
-
const configPath = envConfigPath();
|
|
60553
|
-
writeOut(`Created Caplets config at ${initConfig({
|
|
60554
|
-
...configPath ? { path: configPath } : {},
|
|
60555
|
-
force: Boolean(options.force)
|
|
60556
|
-
})}\n`);
|
|
60557
|
-
});
|
|
60558
|
-
const auth = program.command("auth").description("Manage OAuth credentials for remote servers.");
|
|
60559
|
-
auth.command("login").description("Authenticate a configured remote OAuth server.").argument("<server>", "configured server ID").option("--no-open", "print the authorization URL without opening a browser").action(async (serverId, options) => {
|
|
60560
|
-
await loginAuth(serverId, {
|
|
60561
|
-
noOpen: options.open === false,
|
|
60562
|
-
writeOut,
|
|
60563
|
-
writeErr,
|
|
60564
|
-
...io.authDir ? { authDir: io.authDir } : {}
|
|
60565
|
-
});
|
|
60566
|
-
});
|
|
60567
|
-
auth.command("logout").description("Delete stored OAuth credentials for a server.").argument("<server>", "configured server ID").action((serverId) => {
|
|
60568
|
-
assertLoginTarget(findAuthTarget(serverId), serverId);
|
|
60569
|
-
if (deleteTokenBundle(serverId, io.authDir)) writeOut(`Deleted OAuth credentials for ${serverId}\n`);
|
|
60570
|
-
else writeOut(`No OAuth credentials found for ${serverId}\n`);
|
|
60571
|
-
});
|
|
60572
|
-
auth.command("list").description("List servers with stored OAuth credentials.").action(() => {
|
|
60573
|
-
const servers = authTargets(loadConfig(envConfigPath())).sort((left, right) => left.server.localeCompare(right.server));
|
|
60574
|
-
if (servers.length === 0) {
|
|
60575
|
-
writeOut("No configured remote OAuth servers found.\n");
|
|
60576
|
-
return;
|
|
60577
|
-
}
|
|
60578
|
-
for (const server of servers) {
|
|
60579
|
-
const bundle = readTokenBundle(server.server, io.authDir);
|
|
60580
|
-
const status = !bundle ? "missing" : isTokenBundleExpired(bundle) ? "expired" : "authenticated";
|
|
60581
|
-
writeOut([
|
|
60582
|
-
server.server,
|
|
60583
|
-
status,
|
|
60584
|
-
bundle?.expiresAt ? `expires ${bundle.expiresAt}` : void 0,
|
|
60585
|
-
bundle?.scope ? `scope ${bundle.scope}` : void 0
|
|
60586
|
-
].filter(Boolean).join(" "));
|
|
60587
|
-
writeOut("\n");
|
|
60588
|
-
}
|
|
60589
|
-
});
|
|
60590
|
-
return program;
|
|
60591
|
-
}
|
|
60478
|
+
//#region src/cli/auth.ts
|
|
60592
60479
|
async function loginAuth(serverId, options) {
|
|
60593
|
-
const server = findAuthTarget(serverId, loadConfig(
|
|
60480
|
+
const server = findAuthTarget(serverId, loadConfig(options.configPath));
|
|
60594
60481
|
assertLoginTarget(server, serverId);
|
|
60595
60482
|
try {
|
|
60596
60483
|
const flowOptions = {
|
|
@@ -60607,21 +60494,63 @@ async function loginAuth(serverId, options) {
|
|
|
60607
60494
|
process.exitCode = 1;
|
|
60608
60495
|
}
|
|
60609
60496
|
}
|
|
60610
|
-
function
|
|
60497
|
+
function logoutAuth(serverId, options) {
|
|
60498
|
+
assertLoginTarget(findAuthTarget(serverId, loadConfig(options.configPath)), serverId);
|
|
60499
|
+
if (deleteTokenBundle(serverId, options.authDir)) options.writeOut(`Deleted OAuth credentials for ${serverId}\n`);
|
|
60500
|
+
else options.writeOut(`No OAuth credentials found for ${serverId}\n`);
|
|
60501
|
+
}
|
|
60502
|
+
function listAuth(options) {
|
|
60503
|
+
const servers = authTargets(loadConfig(options.configPath)).sort((left, right) => left.server.localeCompare(right.server));
|
|
60504
|
+
if (servers.length === 0) {
|
|
60505
|
+
options.writeOut("No configured remote OAuth servers found.\n");
|
|
60506
|
+
return;
|
|
60507
|
+
}
|
|
60508
|
+
for (const server of servers) {
|
|
60509
|
+
const bundle = readTokenBundle(server.server, options.authDir);
|
|
60510
|
+
const status = !bundle ? "missing" : isTokenBundleExpired(bundle) ? "expired" : "authenticated";
|
|
60511
|
+
options.writeOut([
|
|
60512
|
+
server.server,
|
|
60513
|
+
status,
|
|
60514
|
+
bundle?.expiresAt ? `expires ${bundle.expiresAt}` : void 0,
|
|
60515
|
+
bundle?.scope ? `scope ${bundle.scope}` : void 0
|
|
60516
|
+
].filter(Boolean).join(" "));
|
|
60517
|
+
options.writeOut("\n");
|
|
60518
|
+
}
|
|
60519
|
+
}
|
|
60520
|
+
function findAuthTarget(serverId, config = loadConfig()) {
|
|
60611
60521
|
return authTargets(config).find((server) => server.server === serverId);
|
|
60612
60522
|
}
|
|
60613
60523
|
function authTargets(config) {
|
|
60614
|
-
const graphqlEndpoints = config.graphqlEndpoints;
|
|
60615
60524
|
return [
|
|
60616
60525
|
...Object.values(config.mcpServers).filter((server) => server.transport !== "stdio" && (server.auth?.type === "oauth2" || server.auth?.type === "oidc")),
|
|
60617
60526
|
...Object.values(config.openapiEndpoints).filter((endpoint) => endpoint.auth?.type === "oauth2" || endpoint.auth?.type === "oidc"),
|
|
60618
|
-
...Object.values(graphqlEndpoints
|
|
60527
|
+
...Object.values(config.graphqlEndpoints).filter((endpoint) => endpoint.auth?.type === "oauth2" || endpoint.auth?.type === "oidc").map(graphQlAuthTarget)
|
|
60619
60528
|
];
|
|
60620
60529
|
}
|
|
60530
|
+
function graphQlAuthTarget(endpoint) {
|
|
60531
|
+
return {
|
|
60532
|
+
...endpoint,
|
|
60533
|
+
url: endpoint.endpointUrl
|
|
60534
|
+
};
|
|
60535
|
+
}
|
|
60621
60536
|
function assertLoginTarget(target, serverId) {
|
|
60622
60537
|
if (!target) throw new CapletsError("SERVER_NOT_FOUND", `Server ${serverId} is not configured for OAuth`);
|
|
60623
60538
|
if ("disabled" in target && target.disabled) throw new CapletsError("SERVER_UNAVAILABLE", `Server ${serverId} is disabled`);
|
|
60624
60539
|
}
|
|
60540
|
+
async function maybeReadManualInput() {
|
|
60541
|
+
if (!stdin.isTTY) return;
|
|
60542
|
+
const rl = createInterface({
|
|
60543
|
+
input: stdin,
|
|
60544
|
+
output: stdout
|
|
60545
|
+
});
|
|
60546
|
+
try {
|
|
60547
|
+
return (await rl.question("Paste callback URL or authorization code after completing authorization, or press Enter to wait for loopback callback: ")).trim() || void 0;
|
|
60548
|
+
} finally {
|
|
60549
|
+
rl.close();
|
|
60550
|
+
}
|
|
60551
|
+
}
|
|
60552
|
+
//#endregion
|
|
60553
|
+
//#region src/cli/init.ts
|
|
60625
60554
|
function initConfig(options = {}) {
|
|
60626
60555
|
const path = resolveConfigPath(options.path);
|
|
60627
60556
|
if (existsSync(path) && !options.force) throw new CapletsError("CONFIG_EXISTS", `Caplets config already exists at ${path}; pass --force to overwrite it`);
|
|
@@ -60653,21 +60582,306 @@ function starterConfig() {
|
|
|
60653
60582
|
} }
|
|
60654
60583
|
}, null, 2);
|
|
60655
60584
|
}
|
|
60656
|
-
|
|
60657
|
-
|
|
60585
|
+
//#endregion
|
|
60586
|
+
//#region src/cli/inspection.ts
|
|
60587
|
+
function listCaplets(config, options) {
|
|
60588
|
+
return allCaplets(config).filter((server) => options.includeDisabled || !server.disabled).map((server) => ({
|
|
60589
|
+
server: server.server,
|
|
60590
|
+
backend: server.backend,
|
|
60591
|
+
name: server.name,
|
|
60592
|
+
description: server.description,
|
|
60593
|
+
disabled: server.disabled,
|
|
60594
|
+
status: initialServerStatus(server)
|
|
60595
|
+
})).sort((left, right) => left.server.localeCompare(right.server));
|
|
60658
60596
|
}
|
|
60659
|
-
|
|
60660
|
-
|
|
60661
|
-
|
|
60662
|
-
|
|
60663
|
-
|
|
60664
|
-
|
|
60597
|
+
function initialServerStatus(server) {
|
|
60598
|
+
return server.disabled ? "disabled" : "not_started";
|
|
60599
|
+
}
|
|
60600
|
+
function allCaplets(config) {
|
|
60601
|
+
return [
|
|
60602
|
+
...Object.values(config.mcpServers),
|
|
60603
|
+
...Object.values(config.openapiEndpoints),
|
|
60604
|
+
...Object.values(config.graphqlEndpoints)
|
|
60605
|
+
];
|
|
60606
|
+
}
|
|
60607
|
+
function formatCapletList(rows) {
|
|
60608
|
+
if (rows.length === 0) return "No configured Caplets found.\n";
|
|
60609
|
+
return `${formatTable([[
|
|
60610
|
+
"server",
|
|
60611
|
+
"backend",
|
|
60612
|
+
"status",
|
|
60613
|
+
"name"
|
|
60614
|
+
], ...rows.map((row) => [
|
|
60615
|
+
row.server,
|
|
60616
|
+
row.backend,
|
|
60617
|
+
row.status,
|
|
60618
|
+
row.name
|
|
60619
|
+
])])}\n`;
|
|
60620
|
+
}
|
|
60621
|
+
function resolveCliConfigPaths(envConfigPath, authDir) {
|
|
60622
|
+
const configPath = resolveConfigPath(envConfigPath);
|
|
60623
|
+
return {
|
|
60624
|
+
userConfig: configPath,
|
|
60625
|
+
projectConfig: resolveProjectConfigPath(),
|
|
60626
|
+
userRoot: resolveCapletsRoot(configPath),
|
|
60627
|
+
projectRoot: resolveProjectCapletsRoot(),
|
|
60628
|
+
authDir: authDir ?? DEFAULT_AUTH_DIR,
|
|
60629
|
+
envConfig: envConfigPath ?? null,
|
|
60630
|
+
projectCapletsTrusted: isTrustedProjectCapletsEnabled()
|
|
60631
|
+
};
|
|
60632
|
+
}
|
|
60633
|
+
function formatConfigPaths(paths) {
|
|
60634
|
+
return [
|
|
60635
|
+
`userConfig: ${paths.userConfig}`,
|
|
60636
|
+
`projectConfig: ${paths.projectConfig}`,
|
|
60637
|
+
`userRoot: ${paths.userRoot}`,
|
|
60638
|
+
`projectRoot: ${paths.projectRoot}`,
|
|
60639
|
+
`authDir: ${paths.authDir}`,
|
|
60640
|
+
`envConfig: ${paths.envConfig ?? "unset"}`,
|
|
60641
|
+
`projectCapletsTrusted: ${paths.projectCapletsTrusted}`
|
|
60642
|
+
].join("\n") + "\n";
|
|
60643
|
+
}
|
|
60644
|
+
function isTrustedProjectCapletsEnabled() {
|
|
60645
|
+
return isTrustedEnvEnabled(process.env[TRUST_PROJECT_CAPLETS_ENV]);
|
|
60646
|
+
}
|
|
60647
|
+
function formatTable(rows) {
|
|
60648
|
+
const firstRow = rows[0];
|
|
60649
|
+
if (!firstRow) return "";
|
|
60650
|
+
const widths = firstRow.map((_, column) => Math.max(...rows.map((row) => row[column]?.length ?? 0)));
|
|
60651
|
+
return rows.map((row) => formatTableRow(row, widths)).join("\n");
|
|
60652
|
+
}
|
|
60653
|
+
function formatTableRow(row, widths) {
|
|
60654
|
+
return row.map((value, column) => {
|
|
60655
|
+
if (column === row.length - 1) return value;
|
|
60656
|
+
return value.padEnd((widths[column] ?? 0) + 2);
|
|
60657
|
+
}).join("").trimEnd();
|
|
60658
|
+
}
|
|
60659
|
+
//#endregion
|
|
60660
|
+
//#region src/cli/install.ts
|
|
60661
|
+
function installCaplets(repo, options = {}) {
|
|
60662
|
+
const source = resolveInstallSource(repo);
|
|
60665
60663
|
try {
|
|
60666
|
-
|
|
60664
|
+
const sourceRoot = join(source.repoRoot, "caplets");
|
|
60665
|
+
if (!existsSync(sourceRoot) || !statSync(sourceRoot).isDirectory()) throw new CapletsError("CONFIG_NOT_FOUND", `No caplets directory found at ${sourceRoot}`);
|
|
60666
|
+
const selectedIds = new Set(options.capletIds ?? []);
|
|
60667
|
+
const destinationRoot = options.destinationRoot ?? resolveCapletsRoot(resolveConfigPath());
|
|
60668
|
+
const available = selectedIds.size === 0 ? discoverCapletFiles(sourceRoot) : discoverSelectedCapletFiles(sourceRoot, selectedIds);
|
|
60669
|
+
const selected = available.filter((caplet) => selectedIds.size === 0 || selectedIds.has(caplet.id));
|
|
60670
|
+
const missing = [...selectedIds].filter((id) => !available.some((caplet) => caplet.id === id));
|
|
60671
|
+
if (missing.length > 0) throw new CapletsError("CONFIG_NOT_FOUND", `Caplet ${missing.join(", ")} not found in ${sourceRoot}`);
|
|
60672
|
+
if (selected.length === 0) throw new CapletsError("CONFIG_NOT_FOUND", `No Caplets found in ${sourceRoot}`);
|
|
60673
|
+
for (const caplet of selected) validateCapletFile(caplet.path);
|
|
60674
|
+
return { installed: preflightInstallCaplets(selected, {
|
|
60675
|
+
destinationRoot,
|
|
60676
|
+
force: Boolean(options.force),
|
|
60677
|
+
repoRoot: source.repoRoot,
|
|
60678
|
+
sourceId: source.id
|
|
60679
|
+
}).map((plan) => installOneCaplet(plan, { force: Boolean(options.force) })) };
|
|
60667
60680
|
} finally {
|
|
60668
|
-
|
|
60681
|
+
source.cleanup();
|
|
60682
|
+
}
|
|
60683
|
+
}
|
|
60684
|
+
function discoverSelectedCapletFiles(sourceRoot, selectedIds) {
|
|
60685
|
+
const candidates = [];
|
|
60686
|
+
for (const id of selectedIds) {
|
|
60687
|
+
if (!SERVER_ID_PATTERN.test(id)) continue;
|
|
60688
|
+
const filePath = join(sourceRoot, `${id}.md`);
|
|
60689
|
+
if (existsSync(filePath) && statSync(filePath).isFile()) candidates.push({
|
|
60690
|
+
id,
|
|
60691
|
+
path: filePath
|
|
60692
|
+
});
|
|
60693
|
+
const directoryPath = join(sourceRoot, id, "CAPLET.md");
|
|
60694
|
+
if (existsSync(directoryPath) && statSync(directoryPath).isFile()) candidates.push({
|
|
60695
|
+
id,
|
|
60696
|
+
path: directoryPath
|
|
60697
|
+
});
|
|
60698
|
+
}
|
|
60699
|
+
return candidates.sort((left, right) => left.id.localeCompare(right.id));
|
|
60700
|
+
}
|
|
60701
|
+
function resolveInstallSource(repo) {
|
|
60702
|
+
if (existsSync(repo) && statSync(repo).isDirectory()) return {
|
|
60703
|
+
id: repo,
|
|
60704
|
+
repoRoot: repo,
|
|
60705
|
+
cleanup: () => {}
|
|
60706
|
+
};
|
|
60707
|
+
const normalizedRepo = normalizeGitRepo(repo);
|
|
60708
|
+
const repoRoot = mkdtempSync(join(tmpdir(), "caplets-install-"));
|
|
60709
|
+
try {
|
|
60710
|
+
execFileSync("git", [
|
|
60711
|
+
"clone",
|
|
60712
|
+
"--depth",
|
|
60713
|
+
"1",
|
|
60714
|
+
"--",
|
|
60715
|
+
normalizedRepo,
|
|
60716
|
+
repoRoot
|
|
60717
|
+
], {
|
|
60718
|
+
stdio: "ignore",
|
|
60719
|
+
timeout: 6e4
|
|
60720
|
+
});
|
|
60721
|
+
return {
|
|
60722
|
+
id: normalizedRepo,
|
|
60723
|
+
repoRoot,
|
|
60724
|
+
cleanup: () => rmSync(repoRoot, {
|
|
60725
|
+
recursive: true,
|
|
60726
|
+
force: true
|
|
60727
|
+
})
|
|
60728
|
+
};
|
|
60729
|
+
} catch (error) {
|
|
60730
|
+
rmSync(repoRoot, {
|
|
60731
|
+
recursive: true,
|
|
60732
|
+
force: true
|
|
60733
|
+
});
|
|
60734
|
+
throw new CapletsError("CONFIG_NOT_FOUND", `Could not clone repo ${repo}`, toSafeError(error));
|
|
60735
|
+
}
|
|
60736
|
+
}
|
|
60737
|
+
function normalizeGitRepo(repo) {
|
|
60738
|
+
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) return `https://github.com/${repo.endsWith(".git") ? repo.slice(0, -4) : repo}.git`;
|
|
60739
|
+
return repo;
|
|
60740
|
+
}
|
|
60741
|
+
function preflightInstallCaplets(caplets, options) {
|
|
60742
|
+
const plans = caplets.map((caplet) => installPlan(caplet, options));
|
|
60743
|
+
for (const plan of plans) if (existsSync(plan.destination) && !options.force) throw new CapletsError("CONFIG_EXISTS", `Caplet ${plan.id} already exists at ${plan.destination}; pass --force to overwrite it`);
|
|
60744
|
+
accessSync(nearestExistingParent(options.destinationRoot), constants.W_OK);
|
|
60745
|
+
for (const plan of plans) accessSync(existsSync(plan.destination) ? dirname(plan.destination) : nearestExistingParent(dirname(plan.destination)), constants.W_OK);
|
|
60746
|
+
mkdirSync(options.destinationRoot, {
|
|
60747
|
+
recursive: true,
|
|
60748
|
+
mode: 448
|
|
60749
|
+
});
|
|
60750
|
+
return plans;
|
|
60751
|
+
}
|
|
60752
|
+
function installPlan(caplet, options) {
|
|
60753
|
+
const isDirectory = basename(caplet.path) === "CAPLET.md";
|
|
60754
|
+
const sourcePath = isDirectory ? dirname(caplet.path) : caplet.path;
|
|
60755
|
+
const sourcePathRelative = relative(options.repoRoot, sourcePath);
|
|
60756
|
+
const destination = isDirectory ? join(options.destinationRoot, caplet.id) : join(options.destinationRoot, `${caplet.id}.md`);
|
|
60757
|
+
return {
|
|
60758
|
+
id: caplet.id,
|
|
60759
|
+
source: `${options.sourceId}#${sourcePathRelative}`,
|
|
60760
|
+
sourcePath,
|
|
60761
|
+
destination,
|
|
60762
|
+
kind: isDirectory ? "directory" : "file"
|
|
60763
|
+
};
|
|
60764
|
+
}
|
|
60765
|
+
function installOneCaplet(plan, options) {
|
|
60766
|
+
if (existsSync(plan.destination)) {
|
|
60767
|
+
if (!options.force) throw new CapletsError("CONFIG_EXISTS", `Caplet ${plan.id} already exists at ${plan.destination}; pass --force to overwrite it`);
|
|
60768
|
+
rmSync(plan.destination, {
|
|
60769
|
+
recursive: true,
|
|
60770
|
+
force: true
|
|
60771
|
+
});
|
|
60772
|
+
}
|
|
60773
|
+
cpSync(plan.sourcePath, plan.destination, {
|
|
60774
|
+
recursive: plan.kind === "directory",
|
|
60775
|
+
force: false,
|
|
60776
|
+
errorOnExist: true
|
|
60777
|
+
});
|
|
60778
|
+
return {
|
|
60779
|
+
id: plan.id,
|
|
60780
|
+
source: plan.source,
|
|
60781
|
+
destination: plan.destination,
|
|
60782
|
+
kind: plan.kind
|
|
60783
|
+
};
|
|
60784
|
+
}
|
|
60785
|
+
function nearestExistingParent(path) {
|
|
60786
|
+
if (existsSync(path)) return path;
|
|
60787
|
+
const parent = dirname(path);
|
|
60788
|
+
if (parent === path) return parent;
|
|
60789
|
+
return nearestExistingParent(parent);
|
|
60790
|
+
}
|
|
60791
|
+
//#endregion
|
|
60792
|
+
//#region src/cli.ts
|
|
60793
|
+
async function runCli(args, io = {}) {
|
|
60794
|
+
const program = createProgram(io);
|
|
60795
|
+
try {
|
|
60796
|
+
await program.parseAsync([
|
|
60797
|
+
"node",
|
|
60798
|
+
"caplets",
|
|
60799
|
+
...args
|
|
60800
|
+
]);
|
|
60801
|
+
} catch (error) {
|
|
60802
|
+
if (error instanceof CommanderError) {
|
|
60803
|
+
if (error.code === "commander.helpDisplayed" || error.code === "commander.version") return;
|
|
60804
|
+
throw new CapletsError("REQUEST_INVALID", error.message);
|
|
60805
|
+
}
|
|
60806
|
+
throw error;
|
|
60669
60807
|
}
|
|
60670
60808
|
}
|
|
60809
|
+
function createProgram(io = {}) {
|
|
60810
|
+
const writeOut = io.writeOut ?? ((value) => process.stdout.write(value));
|
|
60811
|
+
const writeErr = io.writeErr ?? ((value) => process.stderr.write(value));
|
|
60812
|
+
const program = new Command();
|
|
60813
|
+
program.name("caplets").description("Progressive-disclosure gateway for MCP servers.").version(version).exitOverride().configureOutput({
|
|
60814
|
+
writeOut,
|
|
60815
|
+
writeErr,
|
|
60816
|
+
outputError: (value, write) => write(value)
|
|
60817
|
+
});
|
|
60818
|
+
program.command("init").description("Create a starter Caplets config file.").option("--force", "overwrite an existing config file").action((options) => {
|
|
60819
|
+
const configPath = envConfigPath();
|
|
60820
|
+
writeOut(`Created Caplets config at ${initConfig({
|
|
60821
|
+
...configPath ? { path: configPath } : {},
|
|
60822
|
+
force: Boolean(options.force)
|
|
60823
|
+
})}\n`);
|
|
60824
|
+
});
|
|
60825
|
+
program.command("list").description("List configured Caplets.").option("--all", "include disabled Caplets").option("--json", "print JSON output").action((options) => {
|
|
60826
|
+
const rows = listCaplets(loadConfig(envConfigPath()), { includeDisabled: Boolean(options.all) });
|
|
60827
|
+
if (options.json) {
|
|
60828
|
+
writeOut(`${JSON.stringify(rows, null, 2)}\n`);
|
|
60829
|
+
return;
|
|
60830
|
+
}
|
|
60831
|
+
writeOut(formatCapletList(rows));
|
|
60832
|
+
});
|
|
60833
|
+
program.command("install").description("Install Caplets from a repo's caplets directory.").argument("<repo>", "local repo path, Git URL, or GitHub owner/repo").argument("[caplets...]", "optional Caplet IDs to install").option("--force", "overwrite installed Caplets").action((repo, capletIds, options) => {
|
|
60834
|
+
const result = installCaplets(repo, {
|
|
60835
|
+
capletIds,
|
|
60836
|
+
force: Boolean(options.force),
|
|
60837
|
+
destinationRoot: resolveCapletsRoot(resolveConfigPath(envConfigPath()))
|
|
60838
|
+
});
|
|
60839
|
+
for (const caplet of result.installed) writeOut(`Installed ${caplet.id} to ${caplet.destination}\n`);
|
|
60840
|
+
});
|
|
60841
|
+
const config = program.command("config").description("Inspect Caplets config locations.");
|
|
60842
|
+
config.command("path").description("Print the effective user config path.").action(() => {
|
|
60843
|
+
writeOut(`${resolveConfigPath(envConfigPath())}\n`);
|
|
60844
|
+
});
|
|
60845
|
+
config.command("paths").description("Print resolved Caplets config, root, and auth paths.").option("--json", "print JSON output").action((options) => {
|
|
60846
|
+
const paths = resolveCliConfigPaths(envConfigPath(), io.authDir);
|
|
60847
|
+
if (options.json) {
|
|
60848
|
+
writeOut(`${JSON.stringify(paths, null, 2)}\n`);
|
|
60849
|
+
return;
|
|
60850
|
+
}
|
|
60851
|
+
writeOut(formatConfigPaths(paths));
|
|
60852
|
+
});
|
|
60853
|
+
const auth = program.command("auth").description("Manage OAuth credentials for remote servers.");
|
|
60854
|
+
auth.command("login").description("Authenticate a configured remote OAuth server.").argument("<server>", "configured server ID").option("--no-open", "print the authorization URL without opening a browser").action(async (serverId, options) => {
|
|
60855
|
+
const configPath = envConfigPath();
|
|
60856
|
+
await loginAuth(serverId, {
|
|
60857
|
+
noOpen: options.open === false,
|
|
60858
|
+
writeOut,
|
|
60859
|
+
writeErr,
|
|
60860
|
+
...configPath ? { configPath } : {},
|
|
60861
|
+
...io.authDir ? { authDir: io.authDir } : {}
|
|
60862
|
+
});
|
|
60863
|
+
});
|
|
60864
|
+
auth.command("logout").description("Delete stored OAuth credentials for a server.").argument("<server>", "configured server ID").action((serverId) => {
|
|
60865
|
+
const configPath = envConfigPath();
|
|
60866
|
+
logoutAuth(serverId, {
|
|
60867
|
+
writeOut,
|
|
60868
|
+
...configPath ? { configPath } : {},
|
|
60869
|
+
...io.authDir ? { authDir: io.authDir } : {}
|
|
60870
|
+
});
|
|
60871
|
+
});
|
|
60872
|
+
auth.command("list").description("List servers with stored OAuth credentials.").action(() => {
|
|
60873
|
+
const configPath = envConfigPath();
|
|
60874
|
+
listAuth({
|
|
60875
|
+
writeOut,
|
|
60876
|
+
...configPath ? { configPath } : {},
|
|
60877
|
+
...io.authDir ? { authDir: io.authDir } : {}
|
|
60878
|
+
});
|
|
60879
|
+
});
|
|
60880
|
+
return program;
|
|
60881
|
+
}
|
|
60882
|
+
function envConfigPath() {
|
|
60883
|
+
return process.env.CAPLETS_CONFIG?.trim() || void 0;
|
|
60884
|
+
}
|
|
60671
60885
|
//#endregion
|
|
60672
60886
|
//#region src/index.ts
|
|
60673
60887
|
async function main() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caplets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Progressive disclosure gateway for MCP servers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"caplets",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
|
+
"caplets",
|
|
27
28
|
"schemas",
|
|
28
29
|
"README.md",
|
|
29
30
|
"LICENSE"
|