caplets 0.5.2 → 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 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 { homedir } from "node:os";
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.5.2";
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$1(server.url)) ctx.addIssue({
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$1.test(headerName) || FORBIDDEN_HEADERS$1.has(normalized)) ctx.addIssue({
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$1(endpoint.specUrl)) ctx.addIssue({
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$1(endpoint.baseUrl)) ctx.addIssue({
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$1), capletGraphQlOperationSchema).optional().describe("Configured GraphQL operations keyed by stable tool name."),
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$1(endpoint.endpointUrl)) ctx.addIssue({
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$1(endpoint.schemaUrl)) ctx.addIssue({
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$1.test(headerName) || FORBIDDEN_HEADERS$1.has(normalized)) ctx.addIssue({
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$1.test(id)) throw new CapletsError("CONFIG_INVALID", `Caplet file at ${path} derives invalid ID ${id}; ID must match ^[a-zA-Z0-9_-]{1,64}$`);
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
- function isAllowedRemoteUrl$1(value) {
27848
- const url = new URL(value);
27849
- if (url.protocol === "https:") return true;
27850
- if (url.protocol !== "http:") return false;
27851
- return [
27852
- "localhost",
27853
- "127.0.0.1",
27854
- "[::1]",
27855
- "::1"
27856
- ].includes(url.hostname);
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
- return join(authDir, `${server}.json`);
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
- return JSON.parse(readFileSync(path, "utf8"));
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;
@@ -45578,7 +45573,7 @@ var require_utilities = /* @__PURE__ */ __commonJSMin(((exports) => {
45578
45573
  var _resolveSchemaCoordinate = require_resolveSchemaCoordinate();
45579
45574
  }));
45580
45575
  //#endregion
45581
- //#region src/graphql.ts
45576
+ //#region src/http/utils.ts
45582
45577
  var import_graphql = (/* @__PURE__ */ __commonJSMin(((exports) => {
45583
45578
  Object.defineProperty(exports, "__esModule", { value: true });
45584
45579
  Object.defineProperty(exports, "BREAK", {
@@ -46880,7 +46875,40 @@ var import_graphql = (/* @__PURE__ */ __commonJSMin(((exports) => {
46880
46875
  var _index5 = require_error$1();
46881
46876
  var _index6 = require_utilities();
46882
46877
  })))();
46883
- const MAX_RESPONSE_BYTES$1 = 1024 * 1024;
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
46884
46912
  const GRAPHQL_METHOD = "POST";
46885
46913
  const SCALAR_JSON_SCHEMA = {
46886
46914
  String: { type: "string" },
@@ -46966,7 +46994,7 @@ var GraphQLManager = class {
46966
46994
  challenge: response.headers.get("www-authenticate") ? "[REDACTED]" : void 0,
46967
46995
  ...endpoint.auth.type === "oauth2" || endpoint.auth.type === "oidc" ? { nextAction: "run_caplets_auth_login" } : {}
46968
46996
  });
46969
- const body = parseGraphQlBody(response.headers.get("content-type") ?? "", await readLimitedText$1(response));
46997
+ const body = parseHttpBody(response.headers.get("content-type") ?? "", await readGraphQlText(response));
46970
46998
  const result = {
46971
46999
  status: response.status,
46972
47000
  statusText: response.statusText,
@@ -46982,7 +47010,7 @@ var GraphQLManager = class {
46982
47010
  isError: !response.ok || Boolean(body && typeof body === "object" && "errors" in body && body.errors)
46983
47011
  };
46984
47012
  } catch (error) {
46985
- if (isAbortError$1(error)) throw new CapletsError("TOOL_CALL_TIMEOUT", `GraphQL request timed out for ${endpoint.server}/${toolName}`);
47013
+ if (isAbortError(error)) throw new CapletsError("TOOL_CALL_TIMEOUT", `GraphQL request timed out for ${endpoint.server}/${toolName}`);
46986
47014
  if (error instanceof CapletsError) throw error;
46987
47015
  throw new CapletsError("DOWNSTREAM_TOOL_ERROR", `GraphQL request failed for ${endpoint.server}/${toolName}`, toSafeError(error));
46988
47016
  } finally {
@@ -47058,7 +47086,7 @@ async function loadSchema(endpoint, authDir) {
47058
47086
  }
47059
47087
  const response = await postGraphQl(endpoint, endpoint.endpointUrl, { query: (0, import_graphql.getIntrospectionQuery)() }, authDir);
47060
47088
  if (!response.ok) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "GraphQL introspection request failed", { status: response.status });
47061
- const parsed = JSON.parse(await readLimitedText$1(response));
47089
+ const parsed = JSON.parse(await readGraphQlText(response));
47062
47090
  if (parsed.errors || !parsed.data) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "GraphQL introspection returned errors");
47063
47091
  return (0, import_graphql.buildClientSchema)(parsed.data);
47064
47092
  }
@@ -47240,7 +47268,7 @@ async function postGraphQl(endpoint, url, payload, authDir) {
47240
47268
  body: JSON.stringify(payload)
47241
47269
  });
47242
47270
  } catch (error) {
47243
- if (isAbortError$1(error)) throw new CapletsError("TOOL_CALL_TIMEOUT", "GraphQL request timed out");
47271
+ if (isAbortError(error)) throw new CapletsError("TOOL_CALL_TIMEOUT", "GraphQL request timed out");
47244
47272
  throw error;
47245
47273
  } finally {
47246
47274
  clearTimeout(timeout);
@@ -47257,14 +47285,14 @@ async function fetchGraphQlText(endpoint, url, authDir, sendAuth = true) {
47257
47285
  headers: { ...sendAuth ? schemaAuthHeaders(endpoint, authDir) : {} }
47258
47286
  });
47259
47287
  } catch (error) {
47260
- if (isAbortError$1(error)) throw new CapletsError("TOOL_CALL_TIMEOUT", "GraphQL schema request timed out");
47288
+ if (isAbortError(error)) throw new CapletsError("TOOL_CALL_TIMEOUT", "GraphQL schema request timed out");
47261
47289
  throw error;
47262
47290
  } finally {
47263
47291
  clearTimeout(timeout);
47264
47292
  }
47265
47293
  if (response.status >= 300 && response.status < 400) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "GraphQL schema request returned a redirect");
47266
47294
  if (!response.ok) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "GraphQL schema request failed", { status: response.status });
47267
- return readLimitedText$1(response);
47295
+ return readGraphQlText(response);
47268
47296
  }
47269
47297
  function schemaAuthHeaders(endpoint, authDir) {
47270
47298
  return {
@@ -47286,48 +47314,13 @@ function staticHeaders(endpoint) {
47286
47314
  function shouldSendSchemaAuth(endpoint) {
47287
47315
  return Boolean(endpoint.schemaUrl && new URL(endpoint.schemaUrl).origin === new URL(endpoint.endpointUrl).origin);
47288
47316
  }
47289
- function parseGraphQlBody(contentType, text) {
47290
- if (!text) return;
47291
- if (!contentType.includes("application/json")) return text;
47292
- try {
47293
- return JSON.parse(text);
47294
- } catch {
47295
- return text;
47296
- }
47297
- }
47298
- async function readLimitedText$1(response) {
47299
- if (!response.body) return "";
47300
- const reader = response.body.getReader();
47301
- const chunks = [];
47302
- let bytes = 0;
47303
- while (true) {
47304
- const { done, value } = await reader.read();
47305
- if (done) break;
47306
- if (value) {
47307
- bytes += value.byteLength;
47308
- if (bytes > MAX_RESPONSE_BYTES$1) {
47309
- await reader.cancel();
47310
- throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "GraphQL response exceeded byte limit");
47311
- }
47312
- chunks.push(value);
47313
- }
47314
- }
47315
- return new TextDecoder().decode(Buffer.concat(chunks));
47317
+ async function readGraphQlText(response) {
47318
+ return readLimitedText(response, { errorMessage: "GraphQL response exceeded byte limit" });
47316
47319
  }
47317
47320
  function validateEndpointUrl(value) {
47318
- const url = new URL(value);
47319
- if (url.protocol === "https:") return;
47320
- if (url.protocol === "http:" && [
47321
- "localhost",
47322
- "127.0.0.1",
47323
- "[::1]",
47324
- "::1"
47325
- ].includes(url.hostname)) return;
47321
+ if (isAllowedRemoteUrl(value)) return;
47326
47322
  throw new CapletsError("CONFIG_INVALID", "GraphQL URLs must use https except loopback development urls");
47327
47323
  }
47328
- function isAbortError$1(error) {
47329
- return error instanceof DOMException && error.name === "AbortError";
47330
- }
47331
47324
  function graphQlCacheKey(endpoint) {
47332
47325
  return JSON.stringify({
47333
47326
  endpointUrl: endpoint.endpointUrl,
@@ -56841,7 +56834,6 @@ const HTTP_METHODS = [
56841
56834
  "trace"
56842
56835
  ];
56843
56836
  const JSON_CONTENT_TYPES = ["application/json"];
56844
- const MAX_RESPONSE_BYTES = 1024 * 1024;
56845
56837
  const FORBIDDEN_ARGUMENT_HEADERS = new Set([
56846
56838
  "accept",
56847
56839
  "authorization",
@@ -57162,7 +57154,7 @@ function shouldSendSpecAuth(endpoint) {
57162
57154
  }
57163
57155
  async function readResponse(response) {
57164
57156
  const contentType = response.headers.get("content-type") ?? "";
57165
- const body = parseResponseBody(contentType, await readLimitedText(response));
57157
+ const body = parseHttpBody(contentType, await readLimitedText(response, { errorMessage: "OpenAPI response exceeded byte limit" }));
57166
57158
  return {
57167
57159
  status: response.status,
57168
57160
  statusText: response.statusText,
@@ -57170,34 +57162,6 @@ async function readResponse(response) {
57170
57162
  ...body === void 0 ? {} : { body }
57171
57163
  };
57172
57164
  }
57173
- function parseResponseBody(contentType, text) {
57174
- if (!text) return;
57175
- if (!contentType.includes("application/json")) return text;
57176
- try {
57177
- return JSON.parse(text);
57178
- } catch {
57179
- return text;
57180
- }
57181
- }
57182
- async function readLimitedText(response) {
57183
- if (!response.body) return "";
57184
- const reader = response.body.getReader();
57185
- const chunks = [];
57186
- let bytes = 0;
57187
- while (true) {
57188
- const { done, value } = await reader.read();
57189
- if (done) break;
57190
- if (value) {
57191
- bytes += value.byteLength;
57192
- if (bytes > MAX_RESPONSE_BYTES) {
57193
- await reader.cancel();
57194
- throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "OpenAPI response exceeded byte limit");
57195
- }
57196
- chunks.push(value);
57197
- }
57198
- }
57199
- return new TextDecoder().decode(Buffer.concat(chunks));
57200
- }
57201
57165
  async function fetchWithLimit(url, timeoutMs, headers = {}) {
57202
57166
  const controller = new AbortController();
57203
57167
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -57209,7 +57173,7 @@ async function fetchWithLimit(url, timeoutMs, headers = {}) {
57209
57173
  });
57210
57174
  if (response.status >= 300 && response.status < 400) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "OpenAPI spec request returned a redirect");
57211
57175
  if (!response.ok) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "OpenAPI spec request failed", { status: response.status });
57212
- return await readLimitedText(response);
57176
+ return await readLimitedText(response, { errorMessage: "OpenAPI response exceeded byte limit" });
57213
57177
  } catch (error) {
57214
57178
  if (isAbortError(error)) throw new CapletsError("TOOL_CALL_TIMEOUT", "OpenAPI spec request timed out");
57215
57179
  throw error;
@@ -57220,7 +57184,7 @@ async function fetchWithLimit(url, timeoutMs, headers = {}) {
57220
57184
  function validateOperationBaseUrl(endpoint, base) {
57221
57185
  if (!base) throw new CapletsError("CONFIG_INVALID", `${endpoint.server} is missing OpenAPI baseUrl`);
57222
57186
  if (endpoint.specUrl && !endpoint.baseUrl) throw new CapletsError("CONFIG_INVALID", `${endpoint.server} must configure baseUrl when using remote specUrl`);
57223
- if (!isAllowedRequestBaseUrl(base)) throw new CapletsError("CONFIG_INVALID", `${endpoint.server} OpenAPI baseUrl is not allowed`);
57187
+ if (!isAllowedRemoteUrl(base)) throw new CapletsError("CONFIG_INVALID", `${endpoint.server} OpenAPI baseUrl is not allowed`);
57224
57188
  const url = new URL(base);
57225
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`);
57226
57190
  }
@@ -57230,20 +57194,6 @@ function buildOperationUrl(base, operationPath) {
57230
57194
  baseUrl.pathname = [baseUrl.pathname.replace(/\/+$/, ""), operationPath.replace(/^\/+/, "")].filter(Boolean).join("/");
57231
57195
  return baseUrl;
57232
57196
  }
57233
- function isAbortError(error) {
57234
- return error instanceof DOMException && error.name === "AbortError";
57235
- }
57236
- function isAllowedRequestBaseUrl(value) {
57237
- const url = new URL(value);
57238
- if (url.protocol === "https:") return true;
57239
- if (url.protocol !== "http:") return false;
57240
- return [
57241
- "localhost",
57242
- "127.0.0.1",
57243
- "[::1]",
57244
- "::1"
57245
- ].includes(url.hostname);
57246
- }
57247
57197
  function openApiCacheKey(endpoint) {
57248
57198
  return JSON.stringify({
57249
57199
  specPath: endpoint.specPath,
@@ -60525,75 +60475,9 @@ const { program, createCommand, createArgument, createOption, CommanderError, In
60525
60475
  exports.InvalidOptionArgumentError = InvalidArgumentError;
60526
60476
  })))(), 1)).default;
60527
60477
  //#endregion
60528
- //#region src/cli.ts
60529
- async function runCli(args, io = {}) {
60530
- const program = createProgram(io);
60531
- try {
60532
- await program.parseAsync([
60533
- "node",
60534
- "caplets",
60535
- ...args
60536
- ]);
60537
- } catch (error) {
60538
- if (error instanceof CommanderError) {
60539
- if (error.code === "commander.helpDisplayed" || error.code === "commander.version") return;
60540
- throw new CapletsError("REQUEST_INVALID", error.message);
60541
- }
60542
- throw error;
60543
- }
60544
- }
60545
- function createProgram(io = {}) {
60546
- const writeOut = io.writeOut ?? ((value) => process.stdout.write(value));
60547
- const writeErr = io.writeErr ?? ((value) => process.stderr.write(value));
60548
- const program = new Command();
60549
- program.name("caplets").description("Progressive-disclosure gateway for MCP servers.").exitOverride().configureOutput({
60550
- writeOut,
60551
- writeErr,
60552
- outputError: (value, write) => write(value)
60553
- });
60554
- program.command("init").description("Create a starter Caplets config file.").option("--force", "overwrite an existing config file").action((options) => {
60555
- const configPath = envConfigPath();
60556
- writeOut(`Created Caplets config at ${initConfig({
60557
- ...configPath ? { path: configPath } : {},
60558
- force: Boolean(options.force)
60559
- })}\n`);
60560
- });
60561
- const auth = program.command("auth").description("Manage OAuth credentials for remote servers.");
60562
- 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) => {
60563
- await loginAuth(serverId, {
60564
- noOpen: options.open === false,
60565
- writeOut,
60566
- writeErr,
60567
- ...io.authDir ? { authDir: io.authDir } : {}
60568
- });
60569
- });
60570
- auth.command("logout").description("Delete stored OAuth credentials for a server.").argument("<server>", "configured server ID").action((serverId) => {
60571
- assertLoginTarget(findAuthTarget(serverId), serverId);
60572
- if (deleteTokenBundle(serverId, io.authDir)) writeOut(`Deleted OAuth credentials for ${serverId}\n`);
60573
- else writeOut(`No OAuth credentials found for ${serverId}\n`);
60574
- });
60575
- auth.command("list").description("List servers with stored OAuth credentials.").action(() => {
60576
- const servers = authTargets(loadConfig(envConfigPath())).sort((left, right) => left.server.localeCompare(right.server));
60577
- if (servers.length === 0) {
60578
- writeOut("No configured remote OAuth servers found.\n");
60579
- return;
60580
- }
60581
- for (const server of servers) {
60582
- const bundle = readTokenBundle(server.server, io.authDir);
60583
- const status = !bundle ? "missing" : isTokenBundleExpired(bundle) ? "expired" : "authenticated";
60584
- writeOut([
60585
- server.server,
60586
- status,
60587
- bundle?.expiresAt ? `expires ${bundle.expiresAt}` : void 0,
60588
- bundle?.scope ? `scope ${bundle.scope}` : void 0
60589
- ].filter(Boolean).join(" "));
60590
- writeOut("\n");
60591
- }
60592
- });
60593
- return program;
60594
- }
60478
+ //#region src/cli/auth.ts
60595
60479
  async function loginAuth(serverId, options) {
60596
- const server = findAuthTarget(serverId, loadConfig(envConfigPath()));
60480
+ const server = findAuthTarget(serverId, loadConfig(options.configPath));
60597
60481
  assertLoginTarget(server, serverId);
60598
60482
  try {
60599
60483
  const flowOptions = {
@@ -60610,21 +60494,63 @@ async function loginAuth(serverId, options) {
60610
60494
  process.exitCode = 1;
60611
60495
  }
60612
60496
  }
60613
- function findAuthTarget(serverId, config = loadConfig(envConfigPath())) {
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()) {
60614
60521
  return authTargets(config).find((server) => server.server === serverId);
60615
60522
  }
60616
60523
  function authTargets(config) {
60617
- const graphqlEndpoints = config.graphqlEndpoints;
60618
60524
  return [
60619
60525
  ...Object.values(config.mcpServers).filter((server) => server.transport !== "stdio" && (server.auth?.type === "oauth2" || server.auth?.type === "oidc")),
60620
60526
  ...Object.values(config.openapiEndpoints).filter((endpoint) => endpoint.auth?.type === "oauth2" || endpoint.auth?.type === "oidc"),
60621
- ...Object.values(graphqlEndpoints ?? {}).filter((endpoint) => endpoint.auth?.type === "oauth2" || endpoint.auth?.type === "oidc")
60527
+ ...Object.values(config.graphqlEndpoints).filter((endpoint) => endpoint.auth?.type === "oauth2" || endpoint.auth?.type === "oidc").map(graphQlAuthTarget)
60622
60528
  ];
60623
60529
  }
60530
+ function graphQlAuthTarget(endpoint) {
60531
+ return {
60532
+ ...endpoint,
60533
+ url: endpoint.endpointUrl
60534
+ };
60535
+ }
60624
60536
  function assertLoginTarget(target, serverId) {
60625
60537
  if (!target) throw new CapletsError("SERVER_NOT_FOUND", `Server ${serverId} is not configured for OAuth`);
60626
60538
  if ("disabled" in target && target.disabled) throw new CapletsError("SERVER_UNAVAILABLE", `Server ${serverId} is disabled`);
60627
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
60628
60554
  function initConfig(options = {}) {
60629
60555
  const path = resolveConfigPath(options.path);
60630
60556
  if (existsSync(path) && !options.force) throw new CapletsError("CONFIG_EXISTS", `Caplets config already exists at ${path}; pass --force to overwrite it`);
@@ -60656,21 +60582,306 @@ function starterConfig() {
60656
60582
  } }
60657
60583
  }, null, 2);
60658
60584
  }
60659
- function envConfigPath() {
60660
- return process.env.CAPLETS_CONFIG?.trim() || void 0;
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));
60661
60596
  }
60662
- async function maybeReadManualInput() {
60663
- if (!stdin.isTTY) return;
60664
- const rl = createInterface({
60665
- input: stdin,
60666
- output: stdout
60667
- });
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);
60668
60663
  try {
60669
- return (await rl.question("Paste callback URL or authorization code after completing authorization, or press Enter to wait for loopback callback: ")).trim() || void 0;
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) })) };
60670
60680
  } finally {
60671
- rl.close();
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;
60672
60807
  }
60673
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
+ }
60674
60885
  //#endregion
60675
60886
  //#region src/index.ts
60676
60887
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caplets",
3
- "version": "0.5.2",
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"