@jaypie/mcp 0.8.157 → 0.8.159

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
@@ -44,6 +44,17 @@ cpSync(assets.skills, join("dist", MCP_ASSET_DIRECTORY.SKILLS), { recursive: tru
44
44
 
45
45
  `MCP_BUILTIN_SKILLS_PATH` and `MCP_RELEASE_NOTES_PATH` point at other locations.
46
46
 
47
+ `getMcpBuildInfo` returns the version and commit of the build that packed the markdown:
48
+
49
+ ```typescript
50
+ import { getMcpBuildInfo } from "@jaypie/mcp/assets";
51
+
52
+ const { commit, version, versionString } = getMcpBuildInfo();
53
+ // { commit: "460f95c3", version: "0.8.159", versionString: "@jaypie/mcp@0.8.159#460f95c3" }
54
+ ```
55
+
56
+ `commit` is empty when `PROJECT_COMMIT` was unset at build time. Unbuilt source returns version `0.0.0`.
57
+
47
58
  ### Filtering Tools
48
59
 
49
60
  `createMcpServer`, `mcpExpressHandler`, and `mcpHttpHandler` accept `services`, an allowlist of tool names:
package/dist/assets.d.ts CHANGED
@@ -8,6 +8,14 @@ interface McpAssetPaths {
8
8
  /** Absolute path of the Jaypie skills directory read by `skill` */
9
9
  skills: string;
10
10
  }
11
+ interface McpBuildInfo {
12
+ /** First 8 characters of `PROJECT_COMMIT` at build time; empty when unset */
13
+ commit: string;
14
+ /** `package.json` version at build time */
15
+ version: string;
16
+ /** `@jaypie/mcp@<version>#<commit>`, the string `version` answers */
17
+ versionString: string;
18
+ }
11
19
  /**
12
20
  * Absolute paths of the markdown directories `skill` and `release_notes` read
13
21
  * at runtime
@@ -29,6 +37,21 @@ interface McpAssetPaths {
29
37
  * ```
30
38
  */
31
39
  declare function getMcpAssetPaths(): McpAssetPaths;
40
+ /**
41
+ * Version and commit of the `@jaypie/mcp` build, compiled in at build time
42
+ *
43
+ * A consumer syncing `skills/` and `release-notes/` reads the build that
44
+ * packed them from the same package. Unbuilt source (tests, `tsx`) returns
45
+ * version `0.0.0` and an empty commit.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * import { getMcpBuildInfo } from "@jaypie/mcp/assets";
50
+ *
51
+ * const { commit, version, versionString } = getMcpBuildInfo();
52
+ * ```
53
+ */
54
+ declare function getMcpBuildInfo(): McpBuildInfo;
32
55
 
33
- export { MCP_ASSET_DIRECTORY, getMcpAssetPaths };
34
- export type { McpAssetPaths };
56
+ export { MCP_ASSET_DIRECTORY, getMcpAssetPaths, getMcpBuildInfo };
57
+ export type { McpAssetPaths, McpBuildInfo };
package/dist/assets.js CHANGED
@@ -1,11 +1,8 @@
1
1
  import * as path from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
3
 
4
- // Locate the markdown that ships inside @jaypie/mcp, for build scripts that bundle it
5
- //
6
- //
7
- // Constants
8
- //
4
+ // Locate the markdown that ships inside @jaypie/mcp, and the build that packed it,
5
+ // for build scripts that bundle it
9
6
  const MCP_ASSET_DIRECTORY = {
10
7
  RELEASE_NOTES: "release-notes",
11
8
  SKILLS: "skills",
@@ -42,6 +39,30 @@ function getMcpAssetPaths() {
42
39
  skills: path.join(packageRoot, MCP_ASSET_DIRECTORY.SKILLS),
43
40
  };
44
41
  }
42
+ /**
43
+ * Version and commit of the `@jaypie/mcp` build, compiled in at build time
44
+ *
45
+ * A consumer syncing `skills/` and `release-notes/` reads the build that
46
+ * packed them from the same package. Unbuilt source (tests, `tsx`) returns
47
+ * version `0.0.0` and an empty commit.
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * import { getMcpBuildInfo } from "@jaypie/mcp/assets";
52
+ *
53
+ * const { commit, version, versionString } = getMcpBuildInfo();
54
+ * ```
55
+ */
56
+ function getMcpBuildInfo() {
57
+ return {
58
+ commit: "4e7a07fe"
59
+ ,
60
+ version: "0.8.159"
61
+ ,
62
+ versionString: "@jaypie/mcp@0.8.159#4e7a07fe"
63
+ ,
64
+ };
65
+ }
45
66
 
46
- export { MCP_ASSET_DIRECTORY, getMcpAssetPaths };
67
+ export { MCP_ASSET_DIRECTORY, getMcpAssetPaths, getMcpBuildInfo };
47
68
  //# sourceMappingURL=assets.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assets.js","sources":["../src/assets.ts"],"sourcesContent":["// Locate the markdown that ships inside @jaypie/mcp, for build scripts that bundle it\n\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n//\n//\n// Constants\n//\n\nexport const MCP_ASSET_DIRECTORY = {\n RELEASE_NOTES: \"release-notes\",\n SKILLS: \"skills\",\n} as const;\n\n//\n//\n// Types\n//\n\nexport interface McpAssetPaths {\n /** Absolute path of the release notes directory read by `release_notes` */\n releaseNotes: string;\n /** Absolute path of the Jaypie skills directory read by `skill` */\n skills: string;\n}\n\n//\n//\n// Main\n//\n\n/**\n * Absolute paths of the markdown directories `skill` and `release_notes` read\n * at runtime\n *\n * A bundler (esbuild) inlines the code but not the markdown. Copy each\n * directory beside the bundle under its own name (`skills/`,\n * `release-notes/`) and the docs suite finds it without configuration. Call\n * this from a build script, never from bundled code.\n *\n * @example\n * ```typescript\n * import { cpSync } from \"node:fs\";\n * import { join } from \"node:path\";\n * import { getMcpAssetPaths, MCP_ASSET_DIRECTORY } from \"@jaypie/mcp/assets\";\n *\n * const assets = getMcpAssetPaths();\n * cpSync(assets.skills, join(\"dist\", MCP_ASSET_DIRECTORY.SKILLS), { recursive: true });\n * cpSync(assets.releaseNotes, join(\"dist\", MCP_ASSET_DIRECTORY.RELEASE_NOTES), { recursive: true });\n * ```\n */\nexport function getMcpAssetPaths(): McpAssetPaths {\n // src/assets.ts and dist/assets.js both sit one level below the package root\n const packageRoot = path.join(\n path.dirname(fileURLToPath(import.meta.url)),\n \"..\",\n );\n return {\n releaseNotes: path.join(packageRoot, MCP_ASSET_DIRECTORY.RELEASE_NOTES),\n skills: path.join(packageRoot, MCP_ASSET_DIRECTORY.SKILLS),\n };\n}\n"],"names":[],"mappings":";;;AAAA;AAKA;AACA;AACA;AACA;AAEO,MAAM,mBAAmB,GAAG;AACjC,IAAA,aAAa,EAAE,eAAe;AAC9B,IAAA,MAAM,EAAE,QAAQ;;AAelB;AACA;AACA;AACA;AAEA;;;;;;;;;;;;;;;;;;;AAmBG;SACa,gBAAgB,GAAA;;IAE9B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAC3B,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAC5C,IAAI,CACL;IACD,OAAO;QACL,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,mBAAmB,CAAC,aAAa,CAAC;QACvE,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,mBAAmB,CAAC,MAAM,CAAC;KAC3D;AACH;;;;"}
1
+ {"version":3,"file":"assets.js","sources":["../src/assets.ts"],"sourcesContent":["// Locate the markdown that ships inside @jaypie/mcp, and the build that packed it,\n// for build scripts that bundle it\n\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n//\n//\n// Build-time constants (replaced by @rollup/plugin-replace)\n//\n\ndeclare const __BUILD_COMMIT__: string;\ndeclare const __BUILD_VERSION__: string;\ndeclare const __BUILD_VERSION_STRING__: string;\n\n//\n//\n// Constants\n//\n\nconst UNBUILT_COMMIT = \"\";\nconst UNBUILT_VERSION = \"0.0.0\";\nconst UNBUILT_VERSION_STRING = `@jaypie/mcp@${UNBUILT_VERSION}`;\n\nexport const MCP_ASSET_DIRECTORY = {\n RELEASE_NOTES: \"release-notes\",\n SKILLS: \"skills\",\n} as const;\n\n//\n//\n// Types\n//\n\nexport interface McpAssetPaths {\n /** Absolute path of the release notes directory read by `release_notes` */\n releaseNotes: string;\n /** Absolute path of the Jaypie skills directory read by `skill` */\n skills: string;\n}\n\nexport interface McpBuildInfo {\n /** First 8 characters of `PROJECT_COMMIT` at build time; empty when unset */\n commit: string;\n /** `package.json` version at build time */\n version: string;\n /** `@jaypie/mcp@<version>#<commit>`, the string `version` answers */\n versionString: string;\n}\n\n//\n//\n// Main\n//\n\n/**\n * Absolute paths of the markdown directories `skill` and `release_notes` read\n * at runtime\n *\n * A bundler (esbuild) inlines the code but not the markdown. Copy each\n * directory beside the bundle under its own name (`skills/`,\n * `release-notes/`) and the docs suite finds it without configuration. Call\n * this from a build script, never from bundled code.\n *\n * @example\n * ```typescript\n * import { cpSync } from \"node:fs\";\n * import { join } from \"node:path\";\n * import { getMcpAssetPaths, MCP_ASSET_DIRECTORY } from \"@jaypie/mcp/assets\";\n *\n * const assets = getMcpAssetPaths();\n * cpSync(assets.skills, join(\"dist\", MCP_ASSET_DIRECTORY.SKILLS), { recursive: true });\n * cpSync(assets.releaseNotes, join(\"dist\", MCP_ASSET_DIRECTORY.RELEASE_NOTES), { recursive: true });\n * ```\n */\nexport function getMcpAssetPaths(): McpAssetPaths {\n // src/assets.ts and dist/assets.js both sit one level below the package root\n const packageRoot = path.join(\n path.dirname(fileURLToPath(import.meta.url)),\n \"..\",\n );\n return {\n releaseNotes: path.join(packageRoot, MCP_ASSET_DIRECTORY.RELEASE_NOTES),\n skills: path.join(packageRoot, MCP_ASSET_DIRECTORY.SKILLS),\n };\n}\n\n/**\n * Version and commit of the `@jaypie/mcp` build, compiled in at build time\n *\n * A consumer syncing `skills/` and `release-notes/` reads the build that\n * packed them from the same package. Unbuilt source (tests, `tsx`) returns\n * version `0.0.0` and an empty commit.\n *\n * @example\n * ```typescript\n * import { getMcpBuildInfo } from \"@jaypie/mcp/assets\";\n *\n * const { commit, version, versionString } = getMcpBuildInfo();\n * ```\n */\nexport function getMcpBuildInfo(): McpBuildInfo {\n return {\n commit:\n typeof __BUILD_COMMIT__ !== \"undefined\"\n ? __BUILD_COMMIT__\n : UNBUILT_COMMIT,\n version:\n typeof __BUILD_VERSION__ !== \"undefined\"\n ? __BUILD_VERSION__\n : UNBUILT_VERSION,\n versionString:\n typeof __BUILD_VERSION_STRING__ !== \"undefined\"\n ? __BUILD_VERSION_STRING__\n : UNBUILT_VERSION_STRING,\n };\n}\n"],"names":[],"mappings":";;;AAAA;AACA;AAuBO,MAAM,mBAAmB,GAAG;AACjC,IAAA,aAAa,EAAE,eAAe;AAC9B,IAAA,MAAM,EAAE,QAAQ;;AAwBlB;AACA;AACA;AACA;AAEA;;;;;;;;;;;;;;;;;;;AAmBG;SACa,gBAAgB,GAAA;;IAE9B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAC3B,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAC5C,IAAI,CACL;IACD,OAAO;QACL,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,mBAAmB,CAAC,aAAa,CAAC;QACvE,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,mBAAmB,CAAC,MAAM,CAAC;KAC3D;AACH;AAEA;;;;;;;;;;;;;AAaG;SACa,eAAe,GAAA;IAC7B,OAAO;AACL,QAAA,MAAM,EAEA;AACF,YAAgB;AACpB,QAAA,OAAO,EAED;AACF,YAAiB;AACrB,QAAA,aAAa,EAEP;AACF,YAAwB;KAC7B;AACH;;;;"}
@@ -1 +1 @@
1
- export declare const RELEASE_NOTES_HELP = "# Release Notes\n\nBrowse Jaypie package release notes.\n\n## Commands\n\n| Command | Description | Required Parameters |\n|---------|-------------|---------------------|\n| `list` | List available release notes | - |\n| `read` | Read a specific release note | `package`, `version` |\n\n## Options for `list`\n\n| Option | Description |\n|--------|-------------|\n| `package` | Filter by package name (e.g., \"mcp\", \"jaypie\") |\n| `since_version` | Only versions newer than this (e.g., \"1.0.0\") |\n\n## Examples\n\n```\nrelease_notes(\"list\")\nrelease_notes(\"list\", { package: \"mcp\" })\nrelease_notes(\"list\", { package: \"jaypie\", since_version: \"2.0.0\" })\nrelease_notes(\"read\", { package: \"mcp\", version: \"0.4.2\" })\n```\n\n## Available Packages\n\nRelease notes are available for all Jaypie packages including:\n- `jaypie` - Main package\n- `mcp` - MCP server\n- `fabric` - Service handlers and adapters\n- `llm` - LLM provider interface\n- And more...\n\nUse `release_notes(\"list\")` to see all available packages and versions.\n";
1
+ export declare const RELEASE_NOTES_HELP = "# Release Notes\n\nBrowse Jaypie package release notes.\n\n## Commands\n\n| Command | Description | Required Parameters |\n|---------|-------------|---------------------|\n| `list` | List available release notes | - |\n| `read` | Read a specific release note | `package`, `version` |\n\n## Options for `list`\n\n| Option | Description |\n|--------|-------------|\n| `cursor` | Opaque cursor from a previous page's `Next page` footer |\n| `limit` | Notes per page (default 50, maximum 200) |\n| `package` | Filter by package name (e.g., \"mcp\", \"jaypie\") |\n| `since_version` | Only versions newer than this (e.g., \"1.0.0\") |\n\n## Pagination\n\n`list` sorts by package ascending, then version descending, and returns one\npage. When more notes exist, the last line is a footer:\n\n```\nNext page: release_notes(\"list\", { cursor: \"...\" })\n```\n\nThe cursor carries `package`, `since_version`, and `limit`, so the footer call\nalone continues the same listing. Pass `limit` with a cursor to change the page\nsize. Passing a `package` or `since_version` that differs from the cursor is an\nerror.\n\n## Examples\n\n```\nrelease_notes(\"list\")\nrelease_notes(\"list\", { limit: 20 })\nrelease_notes(\"list\", { package: \"mcp\" })\nrelease_notes(\"list\", { package: \"jaypie\", since_version: \"2.0.0\" })\nrelease_notes(\"read\", { package: \"mcp\", version: \"0.4.2\" })\n```\n\n## Available Packages\n\nRelease notes are available for all Jaypie packages including:\n- `jaypie` - Main package\n- `mcp` - MCP server\n- `fabric` - Service handlers and adapters\n- `llm` - LLM provider interface\n- And more...\n\nUse `release_notes(\"list\")` to see all available packages and versions.\n";
@@ -14,13 +14,30 @@ Browse Jaypie package release notes.
14
14
 
15
15
  | Option | Description |
16
16
  |--------|-------------|
17
+ | \`cursor\` | Opaque cursor from a previous page's \`Next page\` footer |
18
+ | \`limit\` | Notes per page (default 50, maximum 200) |
17
19
  | \`package\` | Filter by package name (e.g., "mcp", "jaypie") |
18
20
  | \`since_version\` | Only versions newer than this (e.g., "1.0.0") |
19
21
 
22
+ ## Pagination
23
+
24
+ \`list\` sorts by package ascending, then version descending, and returns one
25
+ page. When more notes exist, the last line is a footer:
26
+
27
+ \`\`\`
28
+ Next page: release_notes("list", { cursor: "..." })
29
+ \`\`\`
30
+
31
+ The cursor carries \`package\`, \`since_version\`, and \`limit\`, so the footer call
32
+ alone continues the same listing. Pass \`limit\` with a cursor to change the page
33
+ size. Passing a \`package\` or \`since_version\` that differs from the cursor is an
34
+ error.
35
+
20
36
  ## Examples
21
37
 
22
38
  \`\`\`
23
39
  release_notes("list")
40
+ release_notes("list", { limit: 20 })
24
41
  release_notes("list", { package: "mcp" })
25
42
  release_notes("list", { package: "jaypie", since_version: "2.0.0" })
26
43
  release_notes("read", { package: "mcp", version: "0.4.2" })
@@ -1 +1 @@
1
- {"version":3,"file":"help.js","sources":["../../../src/suites/docs/help.ts"],"sourcesContent":["// Inlined so bundled builds (esbuild, Lambda) need no shipped file\n\nexport const RELEASE_NOTES_HELP = `# Release Notes\n\nBrowse Jaypie package release notes.\n\n## Commands\n\n| Command | Description | Required Parameters |\n|---------|-------------|---------------------|\n| \\`list\\` | List available release notes | - |\n| \\`read\\` | Read a specific release note | \\`package\\`, \\`version\\` |\n\n## Options for \\`list\\`\n\n| Option | Description |\n|--------|-------------|\n| \\`package\\` | Filter by package name (e.g., \"mcp\", \"jaypie\") |\n| \\`since_version\\` | Only versions newer than this (e.g., \"1.0.0\") |\n\n## Examples\n\n\\`\\`\\`\nrelease_notes(\"list\")\nrelease_notes(\"list\", { package: \"mcp\" })\nrelease_notes(\"list\", { package: \"jaypie\", since_version: \"2.0.0\" })\nrelease_notes(\"read\", { package: \"mcp\", version: \"0.4.2\" })\n\\`\\`\\`\n\n## Available Packages\n\nRelease notes are available for all Jaypie packages including:\n- \\`jaypie\\` - Main package\n- \\`mcp\\` - MCP server\n- \\`fabric\\` - Service handlers and adapters\n- \\`llm\\` - LLM provider interface\n- And more...\n\nUse \\`release_notes(\"list\")\\` to see all available packages and versions.\n`;\n"],"names":[],"mappings":"AAAA;AAEO,MAAM,kBAAkB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"help.js","sources":["../../../src/suites/docs/help.ts"],"sourcesContent":["// Inlined so bundled builds (esbuild, Lambda) need no shipped file\n\nexport const RELEASE_NOTES_HELP = `# Release Notes\n\nBrowse Jaypie package release notes.\n\n## Commands\n\n| Command | Description | Required Parameters |\n|---------|-------------|---------------------|\n| \\`list\\` | List available release notes | - |\n| \\`read\\` | Read a specific release note | \\`package\\`, \\`version\\` |\n\n## Options for \\`list\\`\n\n| Option | Description |\n|--------|-------------|\n| \\`cursor\\` | Opaque cursor from a previous page's \\`Next page\\` footer |\n| \\`limit\\` | Notes per page (default 50, maximum 200) |\n| \\`package\\` | Filter by package name (e.g., \"mcp\", \"jaypie\") |\n| \\`since_version\\` | Only versions newer than this (e.g., \"1.0.0\") |\n\n## Pagination\n\n\\`list\\` sorts by package ascending, then version descending, and returns one\npage. When more notes exist, the last line is a footer:\n\n\\`\\`\\`\nNext page: release_notes(\"list\", { cursor: \"...\" })\n\\`\\`\\`\n\nThe cursor carries \\`package\\`, \\`since_version\\`, and \\`limit\\`, so the footer call\nalone continues the same listing. Pass \\`limit\\` with a cursor to change the page\nsize. Passing a \\`package\\` or \\`since_version\\` that differs from the cursor is an\nerror.\n\n## Examples\n\n\\`\\`\\`\nrelease_notes(\"list\")\nrelease_notes(\"list\", { limit: 20 })\nrelease_notes(\"list\", { package: \"mcp\" })\nrelease_notes(\"list\", { package: \"jaypie\", since_version: \"2.0.0\" })\nrelease_notes(\"read\", { package: \"mcp\", version: \"0.4.2\" })\n\\`\\`\\`\n\n## Available Packages\n\nRelease notes are available for all Jaypie packages including:\n- \\`jaypie\\` - Main package\n- \\`mcp\\` - MCP server\n- \\`fabric\\` - Service handlers and adapters\n- \\`llm\\` - LLM provider interface\n- And more...\n\nUse \\`release_notes(\"list\")\\` to see all available packages and versions.\n`;\n"],"names":[],"mappings":"AAAA;AAEO,MAAM,kBAAkB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -3,6 +3,8 @@ export declare const skillService: import("@jaypie/fabric").Service<{
3
3
  }, string, string>;
4
4
  export declare const versionService: import("@jaypie/fabric").Service<Record<string, unknown>, string, string>;
5
5
  interface ReleaseNotesInput {
6
+ cursor?: string;
7
+ limit?: number | string;
6
8
  package?: string;
7
9
  since_version?: string;
8
10
  version?: string;
@@ -6,15 +6,15 @@ import { existsSync, readdirSync } from 'node:fs';
6
6
  import * as fs from 'node:fs/promises';
7
7
  import * as path from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
- import { gt } from 'semver';
10
- import { getMcpAssetPaths, MCP_ASSET_DIRECTORY } from '../../assets.js';
9
+ import { gt, valid, rcompare } from 'semver';
10
+ import { getMcpAssetPaths, MCP_ASSET_DIRECTORY, getMcpBuildInfo } from '../../assets.js';
11
11
  import { RELEASE_NOTES_HELP } from './help.js';
12
12
 
13
13
  /**
14
14
  * Docs Suite - Documentation services (skill, version, release_notes)
15
15
  */
16
- const BUILD_VERSION_STRING = "@jaypie/mcp@0.8.157#937cc1b9"
17
- ;
16
+ // Build-time constants
17
+ const BUILD_VERSION_STRING = getMcpBuildInfo().versionString;
18
18
  // =============================================================================
19
19
  // ASSET PATHS
20
20
  // =============================================================================
@@ -117,6 +117,102 @@ const skillStore = createLayeredStore({
117
117
  layers: skillLayers,
118
118
  separator: LAYER_SEPARATOR,
119
119
  });
120
+ // =============================================================================
121
+ // HELPER FUNCTIONS
122
+ // =============================================================================
123
+ const CURSOR_ENCODING = "base64url";
124
+ const INVALID_CURSOR_MESSAGE = 'Invalid cursor. Call release_notes("list") to start over.';
125
+ const RELEASE_NOTES_LIST_LIMIT = {
126
+ DEFAULT: 50,
127
+ MAXIMUM: 200,
128
+ };
129
+ function compareText({ a, b }) {
130
+ if (a < b)
131
+ return -1;
132
+ if (a > b)
133
+ return 1;
134
+ return 0;
135
+ }
136
+ /**
137
+ * Package ascending, then semver descending. Valid semver sorts before
138
+ * anything else, and filename breaks every remaining tie so the order is total.
139
+ */
140
+ function compareReleaseNotes({ a, b, }) {
141
+ const byPackage = compareText({ a: a.packageName, b: b.packageName });
142
+ if (byPackage !== 0)
143
+ return byPackage;
144
+ const aVersion = a.version && valid(a.version);
145
+ const bVersion = b.version && valid(b.version);
146
+ if (aVersion && bVersion) {
147
+ const byVersion = rcompare(aVersion, bVersion);
148
+ if (byVersion !== 0)
149
+ return byVersion;
150
+ }
151
+ else if (aVersion) {
152
+ return -1;
153
+ }
154
+ else if (bVersion) {
155
+ return 1;
156
+ }
157
+ return compareText({ a: b.filename, b: a.filename });
158
+ }
159
+ function isPositiveInteger(value) {
160
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
161
+ }
162
+ function isOptionalString(value) {
163
+ return value === undefined || typeof value === "string";
164
+ }
165
+ function encodeListCursor(cursor) {
166
+ const ordered = {
167
+ limit: cursor.limit,
168
+ offset: cursor.offset,
169
+ package: cursor.package,
170
+ since_version: cursor.since_version,
171
+ };
172
+ return Buffer.from(JSON.stringify(ordered)).toString(CURSOR_ENCODING);
173
+ }
174
+ function decodeListCursor(cursor) {
175
+ if (typeof cursor !== "string" || cursor.length === 0) {
176
+ throw new BadRequestError(INVALID_CURSOR_MESSAGE);
177
+ }
178
+ let decoded;
179
+ try {
180
+ decoded = JSON.parse(Buffer.from(cursor, CURSOR_ENCODING).toString("utf-8"));
181
+ }
182
+ catch {
183
+ throw new BadRequestError(INVALID_CURSOR_MESSAGE);
184
+ }
185
+ if (typeof decoded !== "object" || decoded === null) {
186
+ throw new BadRequestError(INVALID_CURSOR_MESSAGE);
187
+ }
188
+ const candidate = decoded;
189
+ const isValid = typeof candidate.offset === "number" &&
190
+ Number.isInteger(candidate.offset) &&
191
+ candidate.offset >= 0 &&
192
+ (candidate.limit === undefined ||
193
+ (isPositiveInteger(candidate.limit) &&
194
+ candidate.limit <= RELEASE_NOTES_LIST_LIMIT.MAXIMUM)) &&
195
+ isOptionalString(candidate.package) &&
196
+ isOptionalString(candidate.since_version);
197
+ if (!isValid) {
198
+ throw new BadRequestError(INVALID_CURSOR_MESSAGE);
199
+ }
200
+ return candidate;
201
+ }
202
+ /**
203
+ * Accept a number or numeric string, reject anything below one, and clamp to
204
+ * the maximum
205
+ */
206
+ function resolveListLimit(value) {
207
+ if (value === undefined || value === null || value === "") {
208
+ return RELEASE_NOTES_LIST_LIMIT.DEFAULT;
209
+ }
210
+ const limit = typeof value === "string" ? Number(value) : value;
211
+ if (!isPositiveInteger(limit)) {
212
+ throw new BadRequestError("limit must be a positive integer");
213
+ }
214
+ return Math.min(limit, RELEASE_NOTES_LIST_LIMIT.MAXIMUM);
215
+ }
120
216
  async function parseReleaseNoteFile(filePath) {
121
217
  try {
122
218
  const content = await fs.readFile(filePath, "utf-8");
@@ -151,21 +247,11 @@ async function getPackageReleaseNotes(packageName) {
151
247
  const packageDir = path.join(RELEASE_NOTES_PATH, packageName);
152
248
  try {
153
249
  const files = await fs.readdir(packageDir);
154
- const mdFiles = files.filter((file) => file.endsWith(".md"));
155
- const notes = await Promise.all(mdFiles.map(async (file) => {
250
+ const mdFiles = files.filter((file) => file.endsWith(MARKDOWN_EXTENSION));
251
+ return await Promise.all(mdFiles.map(async (file) => {
156
252
  const parsed = await parseReleaseNoteFile(path.join(packageDir, file));
157
253
  return { ...parsed, packageName };
158
254
  }));
159
- return notes.sort((a, b) => {
160
- if (!a.version || !b.version)
161
- return 0;
162
- try {
163
- return gt(a.version, b.version) ? -1 : 1;
164
- }
165
- catch {
166
- return b.version.localeCompare(a.version);
167
- }
168
- });
169
255
  }
170
256
  catch {
171
257
  return [];
@@ -198,7 +284,7 @@ const versionService = fabricService({
198
284
  });
199
285
  const releaseNotesService = fabricService({
200
286
  alias: "release_notes",
201
- description: "Browse Jaypie package release notes. Commands: list, read. Call with no args for help.",
287
+ description: "Browse Jaypie package release notes. Commands: list (paginated), read. Call with no args for help.",
202
288
  input: {
203
289
  command: {
204
290
  description: "Command to execute (omit for help)",
@@ -206,7 +292,7 @@ const releaseNotesService = fabricService({
206
292
  type: String,
207
293
  },
208
294
  input: {
209
- description: "Command parameters",
295
+ description: `Command parameters. list: package, since_version, limit (default ${RELEASE_NOTES_LIST_LIMIT.DEFAULT}, maximum ${RELEASE_NOTES_LIST_LIMIT.MAXIMUM}), cursor (from the "Next page" footer). read: package, version (both required).`,
210
296
  required: false,
211
297
  type: Object,
212
298
  },
@@ -219,30 +305,60 @@ const releaseNotesService = fabricService({
219
305
  const p = params || {};
220
306
  switch (command) {
221
307
  case "list": {
308
+ const cursor = p.cursor === undefined ? undefined : decodeListCursor(p.cursor);
309
+ if (cursor) {
310
+ if (p.package !== undefined && p.package !== cursor.package) {
311
+ throw new BadRequestError("package does not match the cursor. Omit package or start over without a cursor.");
312
+ }
313
+ if (p.since_version !== undefined &&
314
+ p.since_version !== cursor.since_version) {
315
+ throw new BadRequestError("since_version does not match the cursor. Omit since_version or start over without a cursor.");
316
+ }
317
+ }
318
+ const limit = resolveListLimit(p.limit ?? cursor?.limit);
319
+ const offset = cursor?.offset ?? 0;
320
+ const packageFilter = cursor ? cursor.package : p.package;
321
+ const sinceVersion = cursor ? cursor.since_version : p.since_version;
222
322
  const entries = await fs.readdir(RELEASE_NOTES_PATH, {
223
323
  withFileTypes: true,
224
324
  });
225
325
  const packageDirs = entries
226
326
  .filter((entry) => entry.isDirectory())
227
327
  .map((entry) => entry.name);
228
- const packagesToList = p.package
229
- ? packageDirs.filter((pkg) => pkg === p.package)
328
+ const packagesToList = packageFilter
329
+ ? packageDirs.filter((pkg) => pkg === packageFilter)
230
330
  : packageDirs;
231
- if (packagesToList.length === 0 && p.package) {
232
- return `No release notes found for package "${p.package}".`;
331
+ if (packagesToList.length === 0 && packageFilter) {
332
+ return `No release notes found for package "${packageFilter}".`;
233
333
  }
234
334
  const allNotes = await Promise.all(packagesToList.map((pkg) => getPackageReleaseNotes(pkg)));
235
335
  let flatNotes = allNotes.flat();
236
- if (p.since_version) {
237
- flatNotes = filterReleaseNotesSince(flatNotes, p.since_version);
336
+ if (sinceVersion) {
337
+ flatNotes = filterReleaseNotesSince(flatNotes, sinceVersion);
238
338
  }
239
339
  if (flatNotes.length === 0) {
240
- const filterDesc = p.since_version
241
- ? ` newer than ${p.since_version}`
242
- : "";
340
+ const filterDesc = sinceVersion ? ` newer than ${sinceVersion}` : "";
243
341
  return `No release notes found${filterDesc}.`;
244
342
  }
245
- return flatNotes.map(formatReleaseNoteListItem).join("\n");
343
+ if (offset >= flatNotes.length) {
344
+ throw new BadRequestError(INVALID_CURSOR_MESSAGE);
345
+ }
346
+ flatNotes.sort((a, b) => compareReleaseNotes({ a, b }));
347
+ const nextOffset = offset + limit;
348
+ const page = flatNotes
349
+ .slice(offset, nextOffset)
350
+ .map(formatReleaseNoteListItem)
351
+ .join("\n");
352
+ if (nextOffset >= flatNotes.length) {
353
+ return page;
354
+ }
355
+ const nextCursor = encodeListCursor({
356
+ limit,
357
+ offset: nextOffset,
358
+ package: packageFilter,
359
+ since_version: sinceVersion,
360
+ });
361
+ return `${page}\n\nNext page: release_notes("list", { cursor: "${nextCursor}" })`;
246
362
  }
247
363
  case "read": {
248
364
  if (!p.package)
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../src/suites/docs/index.ts"],"sourcesContent":["/**\n * Docs Suite - Documentation services (skill, version, release_notes)\n */\nimport { BadRequestError } from \"@jaypie/errors\";\nimport { fabricService } from \"@jaypie/fabric\";\nimport { parseFrontmatter } from \"@jaypie/kit\";\nimport {\n createLayeredStore,\n createMarkdownStore,\n createSkillService,\n type LayeredStoreLayer,\n} from \"@jaypie/tildeskill\";\nimport { existsSync, readdirSync, type Dirent } from \"node:fs\";\nimport * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { gt } from \"semver\";\n\nimport { getMcpAssetPaths, MCP_ASSET_DIRECTORY } from \"../../assets.js\";\nimport { RELEASE_NOTES_HELP } from \"./help.js\";\n\n// Build-time constants\ndeclare const __BUILD_VERSION_STRING__: string;\nconst BUILD_VERSION_STRING =\n typeof __BUILD_VERSION_STRING__ !== \"undefined\"\n ? __BUILD_VERSION_STRING__\n : \"@jaypie/mcp@0.0.0\";\n\n// =============================================================================\n// ASSET PATHS\n// =============================================================================\n\nconst ENV_BUILTIN_SKILLS_PATH = \"MCP_BUILTIN_SKILLS_PATH\";\nconst ENV_RELEASE_NOTES_PATH = \"MCP_RELEASE_NOTES_PATH\";\nconst MARKDOWN_EXTENSION = \".md\";\nconst WARNING_PREFIX = \"[@jaypie/mcp]\";\n\n// A bundler (esbuild) collapses every module into one file, so this module's\n// directory is the bundle directory rather than dist/suites/docs/\nconst MODULE_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));\nconst PACKAGE_ASSET_PATHS = getMcpAssetPaths();\n\n/**\n * An explicit environment path wins outright. Otherwise the package copy wins,\n * then a copy beside the bundle. Returns the package path when neither exists.\n */\nfunction resolveAssetDirectory({\n bundled,\n envPath,\n packaged,\n}: {\n bundled: string;\n envPath?: string;\n packaged: string;\n}): string {\n if (envPath) return envPath;\n return (\n [packaged, bundled].find((candidate) => existsSync(candidate)) ?? packaged\n );\n}\n\nfunction hasEntries({\n directory,\n isEntry,\n}: {\n directory: string;\n isEntry: (entry: Dirent) => boolean;\n}): boolean {\n try {\n return readdirSync(directory, { withFileTypes: true }).some(isEntry);\n } catch {\n return false;\n }\n}\n\n/**\n * Warn once, on first use, when an asset directory is missing or empty. Waiting\n * for first use keeps servers that never call the service quiet.\n */\nfunction createAssetCheck({\n directory,\n envName,\n isEntry,\n name,\n}: {\n directory: string;\n envName: string;\n isEntry: (entry: Dirent) => boolean;\n name: string;\n}): () => void {\n let checked = false;\n return () => {\n if (checked) return;\n checked = true;\n if (hasEntries({ directory, isEntry })) return;\n // stderr keeps the stdio transport's stdout clean and still reaches CloudWatch\n // eslint-disable-next-line no-console\n console.warn(\n `${WARNING_PREFIX} No ${name} found in \"${directory}\". Copy the ${name} directory from getMcpAssetPaths() in @jaypie/mcp/assets beside the bundle or set ${envName}.`,\n );\n };\n}\n\n/** Run `check` before any property of `target` is read */\nfunction withAssetCheck<T extends object>({\n check,\n target,\n}: {\n check: () => void;\n target: T;\n}): T {\n return new Proxy(target, {\n get(object, property, receiver) {\n check();\n return Reflect.get(object, property, receiver);\n },\n });\n}\n\nconst RELEASE_NOTES_PATH = resolveAssetDirectory({\n bundled: path.join(MODULE_DIRECTORY, MCP_ASSET_DIRECTORY.RELEASE_NOTES),\n envPath: process.env[ENV_RELEASE_NOTES_PATH],\n packaged: PACKAGE_ASSET_PATHS.releaseNotes,\n});\n\n// Bundled Jaypie skills ship inside the @jaypie/mcp package. MCP_BUILTIN_SKILLS_PATH\n// relocates them without disabling the built-in layer.\nconst BUILTIN_SKILLS_PATH = resolveAssetDirectory({\n bundled: path.join(MODULE_DIRECTORY, MCP_ASSET_DIRECTORY.SKILLS),\n envPath: process.env[ENV_BUILTIN_SKILLS_PATH],\n packaged: PACKAGE_ASSET_PATHS.skills,\n});\n\nconst checkReleaseNoteAssets = createAssetCheck({\n directory: RELEASE_NOTES_PATH,\n envName: ENV_RELEASE_NOTES_PATH,\n isEntry: (entry) => entry.isDirectory(),\n name: MCP_ASSET_DIRECTORY.RELEASE_NOTES,\n});\n\nconst checkSkillAssets = createAssetCheck({\n directory: BUILTIN_SKILLS_PATH,\n envName: ENV_BUILTIN_SKILLS_PATH,\n isEntry: (entry) => entry.isFile() && entry.name.endsWith(MARKDOWN_EXTENSION),\n name: MCP_ASSET_DIRECTORY.SKILLS,\n});\n\nconst LOCAL_SKILLS_NAMESPACE = \"local\";\nconst JAYPIE_SKILLS_NAMESPACE = \"jaypie\";\nconst LAYER_SEPARATOR = \":\";\n\n// Skill layers resolved in order: a client's MCP_SKILLS_PATH layers on top of\n// the built-in Jaypie skill pack so `skill(\"aws\")` prefers the client's copy\n// while still exposing bundled Jaypie docs under the `jaypie:` namespace.\nconst skillLayers: LayeredStoreLayer[] = [];\n\nif (process.env.MCP_SKILLS_PATH) {\n skillLayers.push({\n namespace: LOCAL_SKILLS_NAMESPACE,\n store: createMarkdownStore({ path: process.env.MCP_SKILLS_PATH }),\n });\n}\n\nskillLayers.push({\n namespace: JAYPIE_SKILLS_NAMESPACE,\n store: withAssetCheck({\n check: checkSkillAssets,\n target: createMarkdownStore({ path: BUILTIN_SKILLS_PATH }),\n }),\n});\n\nconst skillStore = createLayeredStore({\n layers: skillLayers,\n separator: LAYER_SEPARATOR,\n});\n\n// =============================================================================\n// HELPER FUNCTIONS\n// =============================================================================\n\ninterface ReleaseNoteFrontMatter {\n date?: string;\n summary?: string;\n version?: string;\n}\n\nasync function parseReleaseNoteFile(filePath: string): Promise<{\n date?: string;\n filename: string;\n summary?: string;\n version?: string;\n}> {\n try {\n const content = await fs.readFile(filePath, \"utf-8\");\n const filename = path.basename(filePath, \".md\");\n\n if (content.startsWith(\"---\")) {\n const frontMatter =\n parseFrontmatter<ReleaseNoteFrontMatter>(content).data;\n return {\n date: frontMatter.date,\n filename,\n summary: frontMatter.summary,\n version: frontMatter.version || filename,\n };\n }\n\n return { filename, version: filename };\n } catch {\n return { filename: path.basename(filePath, \".md\") };\n }\n}\n\nfunction formatReleaseNoteListItem(note: {\n date?: string;\n filename: string;\n packageName: string;\n summary?: string;\n version?: string;\n}): string {\n const { date, packageName, summary, version } = note;\n const parts = [`* ${packageName}@${version}`];\n\n if (date) {\n parts.push(`(${date})`);\n }\n\n if (summary) {\n parts.push(`- ${summary}`);\n }\n\n return parts.join(\" \");\n}\n\nasync function getPackageReleaseNotes(packageName: string): Promise<\n Array<{\n date?: string;\n filename: string;\n packageName: string;\n summary?: string;\n version?: string;\n }>\n> {\n const packageDir = path.join(RELEASE_NOTES_PATH, packageName);\n try {\n const files = await fs.readdir(packageDir);\n const mdFiles = files.filter((file) => file.endsWith(\".md\"));\n\n const notes = await Promise.all(\n mdFiles.map(async (file) => {\n const parsed = await parseReleaseNoteFile(path.join(packageDir, file));\n return { ...parsed, packageName };\n }),\n );\n\n return notes.sort((a, b) => {\n if (!a.version || !b.version) return 0;\n try {\n return gt(a.version, b.version) ? -1 : 1;\n } catch {\n return b.version.localeCompare(a.version);\n }\n });\n } catch {\n return [];\n }\n}\n\nfunction filterReleaseNotesSince(\n notes: Array<{\n date?: string;\n filename: string;\n packageName: string;\n summary?: string;\n version?: string;\n }>,\n sinceVersion: string,\n): Array<{\n date?: string;\n filename: string;\n packageName: string;\n summary?: string;\n version?: string;\n}> {\n return notes.filter((note) => {\n if (!note.version) return false;\n try {\n return gt(note.version, sinceVersion);\n } catch {\n return false;\n }\n });\n}\n\n// =============================================================================\n// SKILL SERVICE\n// =============================================================================\n\nexport const skillService = createSkillService(skillStore);\n\n// =============================================================================\n// VERSION SERVICE\n// =============================================================================\n\nexport const versionService = fabricService({\n alias: \"version\",\n description: `Prints the current version and hash, \\`${BUILD_VERSION_STRING}\\``,\n input: {},\n service: async () => BUILD_VERSION_STRING,\n});\n\n// =============================================================================\n// RELEASE NOTES SERVICE\n// =============================================================================\n\ninterface ReleaseNotesInput {\n package?: string;\n since_version?: string;\n version?: string;\n}\n\nexport const releaseNotesService = fabricService({\n alias: \"release_notes\",\n description:\n \"Browse Jaypie package release notes. Commands: list, read. Call with no args for help.\",\n input: {\n command: {\n description: \"Command to execute (omit for help)\",\n required: false,\n type: String,\n },\n input: {\n description: \"Command parameters\",\n required: false,\n type: Object,\n },\n },\n service: async ({\n command,\n input: params,\n }: {\n command?: string;\n input?: ReleaseNotesInput;\n }) => {\n if (!command || command === \"help\") {\n return RELEASE_NOTES_HELP;\n }\n\n checkReleaseNoteAssets();\n const p = params || {};\n\n switch (command) {\n case \"list\": {\n const entries = await fs.readdir(RELEASE_NOTES_PATH, {\n withFileTypes: true,\n });\n const packageDirs = entries\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n const packagesToList = p.package\n ? packageDirs.filter((pkg) => pkg === p.package)\n : packageDirs;\n\n if (packagesToList.length === 0 && p.package) {\n return `No release notes found for package \"${p.package}\".`;\n }\n\n const allNotes = await Promise.all(\n packagesToList.map((pkg) => getPackageReleaseNotes(pkg)),\n );\n let flatNotes = allNotes.flat();\n\n if (p.since_version) {\n flatNotes = filterReleaseNotesSince(flatNotes, p.since_version);\n }\n\n if (flatNotes.length === 0) {\n const filterDesc = p.since_version\n ? ` newer than ${p.since_version}`\n : \"\";\n return `No release notes found${filterDesc}.`;\n }\n\n return flatNotes.map(formatReleaseNoteListItem).join(\"\\n\");\n }\n\n case \"read\": {\n if (!p.package) throw new BadRequestError(\"package is required\");\n if (!p.version) throw new BadRequestError(\"version is required\");\n const filePath = path.join(\n RELEASE_NOTES_PATH,\n p.package,\n `${p.version}.md`,\n );\n return fs.readFile(filePath, \"utf-8\");\n }\n\n default:\n throw new BadRequestError(\n `Unknown command: ${command}. Use release_notes() for help.`,\n );\n }\n },\n});\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../src/suites/docs/index.ts"],"sourcesContent":["/**\n * Docs Suite - Documentation services (skill, version, release_notes)\n */\nimport { BadRequestError } from \"@jaypie/errors\";\nimport { fabricService } from \"@jaypie/fabric\";\nimport { parseFrontmatter } from \"@jaypie/kit\";\nimport {\n createLayeredStore,\n createMarkdownStore,\n createSkillService,\n type LayeredStoreLayer,\n} from \"@jaypie/tildeskill\";\nimport { existsSync, readdirSync, type Dirent } from \"node:fs\";\nimport * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { gt, rcompare, valid } from \"semver\";\n\nimport {\n getMcpAssetPaths,\n getMcpBuildInfo,\n MCP_ASSET_DIRECTORY,\n} from \"../../assets.js\";\nimport { RELEASE_NOTES_HELP } from \"./help.js\";\n\n// Build-time constants\nconst BUILD_VERSION_STRING = getMcpBuildInfo().versionString;\n\n// =============================================================================\n// ASSET PATHS\n// =============================================================================\n\nconst ENV_BUILTIN_SKILLS_PATH = \"MCP_BUILTIN_SKILLS_PATH\";\nconst ENV_RELEASE_NOTES_PATH = \"MCP_RELEASE_NOTES_PATH\";\nconst MARKDOWN_EXTENSION = \".md\";\nconst WARNING_PREFIX = \"[@jaypie/mcp]\";\n\n// A bundler (esbuild) collapses every module into one file, so this module's\n// directory is the bundle directory rather than dist/suites/docs/\nconst MODULE_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));\nconst PACKAGE_ASSET_PATHS = getMcpAssetPaths();\n\n/**\n * An explicit environment path wins outright. Otherwise the package copy wins,\n * then a copy beside the bundle. Returns the package path when neither exists.\n */\nfunction resolveAssetDirectory({\n bundled,\n envPath,\n packaged,\n}: {\n bundled: string;\n envPath?: string;\n packaged: string;\n}): string {\n if (envPath) return envPath;\n return (\n [packaged, bundled].find((candidate) => existsSync(candidate)) ?? packaged\n );\n}\n\nfunction hasEntries({\n directory,\n isEntry,\n}: {\n directory: string;\n isEntry: (entry: Dirent) => boolean;\n}): boolean {\n try {\n return readdirSync(directory, { withFileTypes: true }).some(isEntry);\n } catch {\n return false;\n }\n}\n\n/**\n * Warn once, on first use, when an asset directory is missing or empty. Waiting\n * for first use keeps servers that never call the service quiet.\n */\nfunction createAssetCheck({\n directory,\n envName,\n isEntry,\n name,\n}: {\n directory: string;\n envName: string;\n isEntry: (entry: Dirent) => boolean;\n name: string;\n}): () => void {\n let checked = false;\n return () => {\n if (checked) return;\n checked = true;\n if (hasEntries({ directory, isEntry })) return;\n // stderr keeps the stdio transport's stdout clean and still reaches CloudWatch\n // eslint-disable-next-line no-console\n console.warn(\n `${WARNING_PREFIX} No ${name} found in \"${directory}\". Copy the ${name} directory from getMcpAssetPaths() in @jaypie/mcp/assets beside the bundle or set ${envName}.`,\n );\n };\n}\n\n/** Run `check` before any property of `target` is read */\nfunction withAssetCheck<T extends object>({\n check,\n target,\n}: {\n check: () => void;\n target: T;\n}): T {\n return new Proxy(target, {\n get(object, property, receiver) {\n check();\n return Reflect.get(object, property, receiver);\n },\n });\n}\n\nconst RELEASE_NOTES_PATH = resolveAssetDirectory({\n bundled: path.join(MODULE_DIRECTORY, MCP_ASSET_DIRECTORY.RELEASE_NOTES),\n envPath: process.env[ENV_RELEASE_NOTES_PATH],\n packaged: PACKAGE_ASSET_PATHS.releaseNotes,\n});\n\n// Bundled Jaypie skills ship inside the @jaypie/mcp package. MCP_BUILTIN_SKILLS_PATH\n// relocates them without disabling the built-in layer.\nconst BUILTIN_SKILLS_PATH = resolveAssetDirectory({\n bundled: path.join(MODULE_DIRECTORY, MCP_ASSET_DIRECTORY.SKILLS),\n envPath: process.env[ENV_BUILTIN_SKILLS_PATH],\n packaged: PACKAGE_ASSET_PATHS.skills,\n});\n\nconst checkReleaseNoteAssets = createAssetCheck({\n directory: RELEASE_NOTES_PATH,\n envName: ENV_RELEASE_NOTES_PATH,\n isEntry: (entry) => entry.isDirectory(),\n name: MCP_ASSET_DIRECTORY.RELEASE_NOTES,\n});\n\nconst checkSkillAssets = createAssetCheck({\n directory: BUILTIN_SKILLS_PATH,\n envName: ENV_BUILTIN_SKILLS_PATH,\n isEntry: (entry) => entry.isFile() && entry.name.endsWith(MARKDOWN_EXTENSION),\n name: MCP_ASSET_DIRECTORY.SKILLS,\n});\n\nconst LOCAL_SKILLS_NAMESPACE = \"local\";\nconst JAYPIE_SKILLS_NAMESPACE = \"jaypie\";\nconst LAYER_SEPARATOR = \":\";\n\n// Skill layers resolved in order: a client's MCP_SKILLS_PATH layers on top of\n// the built-in Jaypie skill pack so `skill(\"aws\")` prefers the client's copy\n// while still exposing bundled Jaypie docs under the `jaypie:` namespace.\nconst skillLayers: LayeredStoreLayer[] = [];\n\nif (process.env.MCP_SKILLS_PATH) {\n skillLayers.push({\n namespace: LOCAL_SKILLS_NAMESPACE,\n store: createMarkdownStore({ path: process.env.MCP_SKILLS_PATH }),\n });\n}\n\nskillLayers.push({\n namespace: JAYPIE_SKILLS_NAMESPACE,\n store: withAssetCheck({\n check: checkSkillAssets,\n target: createMarkdownStore({ path: BUILTIN_SKILLS_PATH }),\n }),\n});\n\nconst skillStore = createLayeredStore({\n layers: skillLayers,\n separator: LAYER_SEPARATOR,\n});\n\n// =============================================================================\n// HELPER FUNCTIONS\n// =============================================================================\n\nconst CURSOR_ENCODING = \"base64url\";\nconst INVALID_CURSOR_MESSAGE =\n 'Invalid cursor. Call release_notes(\"list\") to start over.';\nconst RELEASE_NOTES_LIST_LIMIT = {\n DEFAULT: 50,\n MAXIMUM: 200,\n} as const;\n\ninterface ReleaseNoteFrontMatter {\n date?: string;\n summary?: string;\n version?: string;\n}\n\ninterface ReleaseNoteListItem {\n date?: string;\n filename: string;\n packageName: string;\n summary?: string;\n version?: string;\n}\n\n/** Decoded form of the opaque `list` cursor */\ninterface ReleaseNotesListCursor {\n limit?: number;\n offset: number;\n package?: string;\n since_version?: string;\n}\n\nfunction compareText({ a, b }: { a: string; b: string }): number {\n if (a < b) return -1;\n if (a > b) return 1;\n return 0;\n}\n\n/**\n * Package ascending, then semver descending. Valid semver sorts before\n * anything else, and filename breaks every remaining tie so the order is total.\n */\nfunction compareReleaseNotes({\n a,\n b,\n}: {\n a: ReleaseNoteListItem;\n b: ReleaseNoteListItem;\n}): number {\n const byPackage = compareText({ a: a.packageName, b: b.packageName });\n if (byPackage !== 0) return byPackage;\n\n const aVersion = a.version && valid(a.version);\n const bVersion = b.version && valid(b.version);\n if (aVersion && bVersion) {\n const byVersion = rcompare(aVersion, bVersion);\n if (byVersion !== 0) return byVersion;\n } else if (aVersion) {\n return -1;\n } else if (bVersion) {\n return 1;\n }\n\n return compareText({ a: b.filename, b: a.filename });\n}\n\nfunction isPositiveInteger(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value > 0;\n}\n\nfunction isOptionalString(value: unknown): boolean {\n return value === undefined || typeof value === \"string\";\n}\n\nfunction encodeListCursor(cursor: ReleaseNotesListCursor): string {\n const ordered: ReleaseNotesListCursor = {\n limit: cursor.limit,\n offset: cursor.offset,\n package: cursor.package,\n since_version: cursor.since_version,\n };\n return Buffer.from(JSON.stringify(ordered)).toString(CURSOR_ENCODING);\n}\n\nfunction decodeListCursor(cursor: unknown): ReleaseNotesListCursor {\n if (typeof cursor !== \"string\" || cursor.length === 0) {\n throw new BadRequestError(INVALID_CURSOR_MESSAGE);\n }\n\n let decoded: unknown;\n try {\n decoded = JSON.parse(\n Buffer.from(cursor, CURSOR_ENCODING).toString(\"utf-8\"),\n );\n } catch {\n throw new BadRequestError(INVALID_CURSOR_MESSAGE);\n }\n\n if (typeof decoded !== \"object\" || decoded === null) {\n throw new BadRequestError(INVALID_CURSOR_MESSAGE);\n }\n\n const candidate = decoded as Record<string, unknown>;\n const isValid =\n typeof candidate.offset === \"number\" &&\n Number.isInteger(candidate.offset) &&\n candidate.offset >= 0 &&\n (candidate.limit === undefined ||\n (isPositiveInteger(candidate.limit) &&\n candidate.limit <= RELEASE_NOTES_LIST_LIMIT.MAXIMUM)) &&\n isOptionalString(candidate.package) &&\n isOptionalString(candidate.since_version);\n\n if (!isValid) {\n throw new BadRequestError(INVALID_CURSOR_MESSAGE);\n }\n\n return candidate as unknown as ReleaseNotesListCursor;\n}\n\n/**\n * Accept a number or numeric string, reject anything below one, and clamp to\n * the maximum\n */\nfunction resolveListLimit(value: unknown): number {\n if (value === undefined || value === null || value === \"\") {\n return RELEASE_NOTES_LIST_LIMIT.DEFAULT;\n }\n const limit = typeof value === \"string\" ? Number(value) : value;\n if (!isPositiveInteger(limit)) {\n throw new BadRequestError(\"limit must be a positive integer\");\n }\n return Math.min(limit, RELEASE_NOTES_LIST_LIMIT.MAXIMUM);\n}\n\nasync function parseReleaseNoteFile(filePath: string): Promise<{\n date?: string;\n filename: string;\n summary?: string;\n version?: string;\n}> {\n try {\n const content = await fs.readFile(filePath, \"utf-8\");\n const filename = path.basename(filePath, \".md\");\n\n if (content.startsWith(\"---\")) {\n const frontMatter =\n parseFrontmatter<ReleaseNoteFrontMatter>(content).data;\n return {\n date: frontMatter.date,\n filename,\n summary: frontMatter.summary,\n version: frontMatter.version || filename,\n };\n }\n\n return { filename, version: filename };\n } catch {\n return { filename: path.basename(filePath, \".md\") };\n }\n}\n\nfunction formatReleaseNoteListItem(note: {\n date?: string;\n filename: string;\n packageName: string;\n summary?: string;\n version?: string;\n}): string {\n const { date, packageName, summary, version } = note;\n const parts = [`* ${packageName}@${version}`];\n\n if (date) {\n parts.push(`(${date})`);\n }\n\n if (summary) {\n parts.push(`- ${summary}`);\n }\n\n return parts.join(\" \");\n}\n\nasync function getPackageReleaseNotes(\n packageName: string,\n): Promise<ReleaseNoteListItem[]> {\n const packageDir = path.join(RELEASE_NOTES_PATH, packageName);\n try {\n const files = await fs.readdir(packageDir);\n const mdFiles = files.filter((file) => file.endsWith(MARKDOWN_EXTENSION));\n\n return await Promise.all(\n mdFiles.map(async (file) => {\n const parsed = await parseReleaseNoteFile(path.join(packageDir, file));\n return { ...parsed, packageName };\n }),\n );\n } catch {\n return [];\n }\n}\n\nfunction filterReleaseNotesSince(\n notes: ReleaseNoteListItem[],\n sinceVersion: string,\n): ReleaseNoteListItem[] {\n return notes.filter((note) => {\n if (!note.version) return false;\n try {\n return gt(note.version, sinceVersion);\n } catch {\n return false;\n }\n });\n}\n\n// =============================================================================\n// SKILL SERVICE\n// =============================================================================\n\nexport const skillService = createSkillService(skillStore);\n\n// =============================================================================\n// VERSION SERVICE\n// =============================================================================\n\nexport const versionService = fabricService({\n alias: \"version\",\n description: `Prints the current version and hash, \\`${BUILD_VERSION_STRING}\\``,\n input: {},\n service: async () => BUILD_VERSION_STRING,\n});\n\n// =============================================================================\n// RELEASE NOTES SERVICE\n// =============================================================================\n\ninterface ReleaseNotesInput {\n cursor?: string;\n limit?: number | string;\n package?: string;\n since_version?: string;\n version?: string;\n}\n\nexport const releaseNotesService = fabricService({\n alias: \"release_notes\",\n description:\n \"Browse Jaypie package release notes. Commands: list (paginated), read. Call with no args for help.\",\n input: {\n command: {\n description: \"Command to execute (omit for help)\",\n required: false,\n type: String,\n },\n input: {\n description: `Command parameters. list: package, since_version, limit (default ${RELEASE_NOTES_LIST_LIMIT.DEFAULT}, maximum ${RELEASE_NOTES_LIST_LIMIT.MAXIMUM}), cursor (from the \"Next page\" footer). read: package, version (both required).`,\n required: false,\n type: Object,\n },\n },\n service: async ({\n command,\n input: params,\n }: {\n command?: string;\n input?: ReleaseNotesInput;\n }) => {\n if (!command || command === \"help\") {\n return RELEASE_NOTES_HELP;\n }\n\n checkReleaseNoteAssets();\n const p = params || {};\n\n switch (command) {\n case \"list\": {\n const cursor =\n p.cursor === undefined ? undefined : decodeListCursor(p.cursor);\n if (cursor) {\n if (p.package !== undefined && p.package !== cursor.package) {\n throw new BadRequestError(\n \"package does not match the cursor. Omit package or start over without a cursor.\",\n );\n }\n if (\n p.since_version !== undefined &&\n p.since_version !== cursor.since_version\n ) {\n throw new BadRequestError(\n \"since_version does not match the cursor. Omit since_version or start over without a cursor.\",\n );\n }\n }\n\n const limit = resolveListLimit(p.limit ?? cursor?.limit);\n const offset = cursor?.offset ?? 0;\n const packageFilter = cursor ? cursor.package : p.package;\n const sinceVersion = cursor ? cursor.since_version : p.since_version;\n\n const entries = await fs.readdir(RELEASE_NOTES_PATH, {\n withFileTypes: true,\n });\n const packageDirs = entries\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n const packagesToList = packageFilter\n ? packageDirs.filter((pkg) => pkg === packageFilter)\n : packageDirs;\n\n if (packagesToList.length === 0 && packageFilter) {\n return `No release notes found for package \"${packageFilter}\".`;\n }\n\n const allNotes = await Promise.all(\n packagesToList.map((pkg) => getPackageReleaseNotes(pkg)),\n );\n let flatNotes = allNotes.flat();\n\n if (sinceVersion) {\n flatNotes = filterReleaseNotesSince(flatNotes, sinceVersion);\n }\n\n if (flatNotes.length === 0) {\n const filterDesc = sinceVersion ? ` newer than ${sinceVersion}` : \"\";\n return `No release notes found${filterDesc}.`;\n }\n\n if (offset >= flatNotes.length) {\n throw new BadRequestError(INVALID_CURSOR_MESSAGE);\n }\n\n flatNotes.sort((a, b) => compareReleaseNotes({ a, b }));\n const nextOffset = offset + limit;\n const page = flatNotes\n .slice(offset, nextOffset)\n .map(formatReleaseNoteListItem)\n .join(\"\\n\");\n\n if (nextOffset >= flatNotes.length) {\n return page;\n }\n\n const nextCursor = encodeListCursor({\n limit,\n offset: nextOffset,\n package: packageFilter,\n since_version: sinceVersion,\n });\n return `${page}\\n\\nNext page: release_notes(\"list\", { cursor: \"${nextCursor}\" })`;\n }\n\n case \"read\": {\n if (!p.package) throw new BadRequestError(\"package is required\");\n if (!p.version) throw new BadRequestError(\"version is required\");\n const filePath = path.join(\n RELEASE_NOTES_PATH,\n p.package,\n `${p.version}.md`,\n );\n return fs.readFile(filePath, \"utf-8\");\n }\n\n default:\n throw new BadRequestError(\n `Unknown command: ${command}. Use release_notes() for help.`,\n );\n }\n },\n});\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaypie/mcp",
3
- "version": "0.8.157",
3
+ "version": "0.8.159",
4
4
  "description": "Jaypie MCP",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,42 @@
1
+ ---
2
+ version: 1.2.86
3
+ date: 2026-09-13
4
+ summary: CDK owns DynamoDB indexes; external secrets and SSOSync secret props keep credentials out of templates
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - **`JaypieMigration`** grants `DescribeTable`, `DescribeTimeToLive`, and
10
+ `DescribeContinuousBackups` only. `UpdateTable`, `UpdateTimeToLive`, and
11
+ `UpdateContinuousBackups` are removed: a GSI a migration creates blocks every
12
+ later CloudFormation update to the table (#552).
13
+ - **`JaypieMigration`** warns at synth when a `JaypieDynamoDb` in `tables`
14
+ declares no `indexes`. Acknowledge
15
+ `@jaypie/constructs:migrationTableWithoutIndexes` for tables that need none.
16
+ - **`JaypieDynamoDb`** sorts `indexes` by name, so
17
+ `indexes: getAllRegisteredIndexes()` synthesizes the same template regardless
18
+ of import order, and exposes the sorted list as `indexes`.
19
+ - **`constructTagger`** and **`JaypieInfrastructureStack`** apply per-build tags
20
+ to DynamoDB tables again, reverting 1.2.85.
21
+ - **`JaypieSecret`** and **`JaypieEnvSecret`** accept `external: true`, which
22
+ creates an empty secret whose value never enters the template, assets
23
+ bucket, `cdk.out`, or `cdk diff`. An `ExternalArn` output described by the
24
+ `envKey` locates the secret for CI `put-secret-value` (#551).
25
+ - **`JaypieSsoSyncApplication`** accepts `googleCredentialsSecret` and
26
+ `scimEndpointAccessTokenSecret` and deploys SSOSync as "App only" with six
27
+ secret ARNs in `CrossStackConfig`. Literal credentials still work and warn
28
+ at synth (`@jaypie/constructs:ssoSyncLiteralCredentials`).
29
+
30
+ ## Upgrade
31
+
32
+ - Declare every registered index in `JaypieDynamoDb` `indexes`.
33
+ - A table with GSIs created outside CDK fails the first deploy on this version,
34
+ because build tags update the table. CloudFormation cannot adopt those
35
+ indexes: declare them and recreate the table. See `skill("dynamodb")`.
36
+ - Migrations that call `UpdateTable`, `UpdateTimeToLive`, or
37
+ `UpdateContinuousBackups` fail with `AccessDenied`. Move those settings into
38
+ `JaypieDynamoDb`.
39
+ - Add indexes to an existing table one per deploy.
40
+ - Moving `JaypieSsoSyncApplication` to secret props deletes the SAR-created
41
+ `SSOSync*` secrets. Sync fails until CI writes the new secret values. See
42
+ `skill("secrets")`.
@@ -0,0 +1,19 @@
1
+ ---
2
+ version: 1.2.87
3
+ date: 2026-09-14
4
+ summary: JaypieStack warns at synth on an unset or non-hex PROJECT_NONCE
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - **`JaypieStack`** warns at synth (`@jaypie/constructs:projectNonceFormat`)
10
+ when `CDK_DEFAULT_ACCOUNT` is set and `PROJECT_NONCE` is unset or does not
11
+ match `^[0-9a-f]{6,}$`. A word or branch-name nonce lands in immutable stack
12
+ names and global S3 bucket names, where it repeats across projects and
13
+ accounts (#554).
14
+
15
+ ## Upgrade
16
+
17
+ - A stack deployed with a non-hex nonce keeps warning. Stack and bucket names
18
+ cannot be renamed: acknowledge `@jaypie/constructs:projectNonceFormat`, or
19
+ deploy a new install with a hex nonce and tear down the old one.
@@ -0,0 +1,19 @@
1
+ ---
2
+ version: 0.8.158
3
+ date: 2026-09-13
4
+ summary: Indexes belong in CDK; external secrets keep credentials out of templates
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - **`dynamodb`** replaces "Keep Table Properties Stable" with "Share Indexes
10
+ Between CDK and Runtime" (`indexes: getAllRegisteredIndexes()`) and "Indexes
11
+ Created Outside CDK" (failure and recovery). Local Development creates the
12
+ table with `createTable()` from the registry and runs the migration seed,
13
+ replacing the AWS CLI script that created no GSIs (#552).
14
+ - **`migrations`** documents describe-only grants, the synth warning for tables
15
+ without indexes, and a seed module shared by the migration and local scripts.
16
+ - **`cdk`** removes the note that per-build tags skip DynamoDB tables.
17
+ - **`secrets`** adds External Secrets (`external: true` with a CI
18
+ `put-secret-value` step) and SSOSync Credentials, and notes that env-sourced
19
+ values are written into the template (#551).