@cyanheads/cyanheads-mcp-server 0.4.2 → 0.4.3
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/AGENTS.md +13 -12
- package/CLAUDE.md +13 -12
- package/Dockerfile +38 -29
- package/README.md +15 -14
- package/changelog/0.4.x/0.4.3.md +34 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/tools/definitions/describe-entry.tool.d.ts +23 -0
- package/dist/mcp-server/tools/definitions/describe-entry.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/describe-entry.tool.js +116 -95
- package/dist/mcp-server/tools/definitions/describe-entry.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-catalog.tool.d.ts +11 -0
- package/dist/mcp-server/tools/definitions/search-catalog.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-catalog.tool.js +82 -20
- package/dist/mcp-server/tools/definitions/search-catalog.tool.js.map +1 -1
- package/dist/services/catalog/catalog-service.d.ts +2 -2
- package/dist/services/catalog/catalog-service.d.ts.map +1 -1
- package/dist/services/catalog/catalog-service.js +29 -11
- package/dist/services/catalog/catalog-service.js.map +1 -1
- package/dist/services/catalog/remote-catalog-provider.d.ts +2 -2
- package/dist/services/catalog/remote-catalog-provider.d.ts.map +1 -1
- package/dist/services/catalog/remote-catalog-provider.js +34 -5
- package/dist/services/catalog/remote-catalog-provider.js.map +1 -1
- package/dist/services/catalog/self-record.js +3 -3
- package/dist/services/catalog/types.d.ts +2 -2
- package/dist/services/catalog/types.d.ts.map +1 -1
- package/manifest.json +1 -1
- package/package.json +10 -9
- package/server.json +3 -3
package/AGENTS.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** cyanheads-mcp-server
|
|
4
|
-
**Version:** 0.4.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.4.3
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.10`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
-
**MCP SDK:** `@modelcontextprotocol/server` 2.
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` 2.1.0
|
|
8
8
|
**Zod:** ^4.6.5
|
|
9
9
|
|
|
10
10
|
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
@@ -20,7 +20,7 @@ A fleet discovery surface for the cyanheads MCP ecosystem. Two tools:
|
|
|
20
20
|
|
|
21
21
|
The catalog (`fleet.json`) is produced by `caseyjhand-portfolio/scripts/build-fleet-json.ts` at portfolio deploy time. Document vectors are baked into the JSON. The server only embeds the user's query at runtime via `@huggingface/transformers`. See `docs/design.md` for the full architecture.
|
|
22
22
|
|
|
23
|
-
The generated catalog does not include this server, so `src/services/catalog/self-record.ts` supplies a static `CatalogRecord` for `cyanheads-mcp-server` that `CatalogService.getServer()` / `getTool()` consult **only after** the remote
|
|
23
|
+
The generated catalog does not include this server, so `src/services/catalog/self-record.ts` supplies a static `CatalogRecord` for `cyanheads-mcp-server` that `CatalogService.getServer()` / `getTool()` consult **only after** the remote namespace has no candidate. Exact remote names win, then unique case-insensitive remote names; ambiguous folds never fall through to self. Output names are canonical. The self record is never part of the vector index, so semantic search does not surface it — only name resolution does.
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
@@ -67,7 +67,7 @@ export const searchItems = tool('search_items', {
|
|
|
67
67
|
annotations: { readOnlyHint: true },
|
|
68
68
|
input: z.object({
|
|
69
69
|
query: z.string().describe('Search terms'),
|
|
70
|
-
limit: z.number().default(10).describe('Max results'),
|
|
70
|
+
limit: z.number().int().min(1).max(100).default(10).describe('Max results (1–100)'),
|
|
71
71
|
}),
|
|
72
72
|
output: z.object({
|
|
73
73
|
items: z.array(z.object({
|
|
@@ -188,13 +188,14 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
188
188
|
| Property | Description |
|
|
189
189
|
|:---------|:------------|
|
|
190
190
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
191
|
-
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
191
|
+
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). |
|
|
192
192
|
| `ctx.requestInput` | Suspend and ask the caller for more input — `return ctx.requestInput({ inputRequests: { key: inputRequired.elicit({ message, requestedSchema }) } })`. Never returns; the handler is re-entered with the answers. Always present. |
|
|
193
|
-
| `ctx.inputs` |
|
|
193
|
+
| `ctx.inputs` | The request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped` — limited to what the client declared (`elicitation` and its form/url modes, `sampling`, `roots`). Client-supplied: a consent gate trusts only a `ctx.state` record it stored when it asked, bound to the operation, caller, and target (see the `api-context` skill). |
|
|
194
|
+
| `ctx.clientCapabilities` | What the client declared for this request, `undefined` when no view exists. Decides whether to ask for optional context (e.g. roots); never a reason to skip a consent prompt. |
|
|
194
195
|
| `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
|
|
195
196
|
| `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
|
|
196
197
|
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
197
|
-
| `ctx.requestId` |
|
|
198
|
+
| `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |
|
|
198
199
|
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
199
200
|
|
|
200
201
|
---
|
|
@@ -203,7 +204,7 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
203
204
|
|
|
204
205
|
Handlers throw — the framework catches, classifies, and formats.
|
|
205
206
|
|
|
206
|
-
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability,
|
|
207
|
+
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. The framework puts it on the wire whenever a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason')` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` text unless the message already contains it verbatim; override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Every error envelope also carries `data.requestId`, the id the server's log records for that call carry, and `content[]` closes with `(reason … · request <id>)`. Mark an entry the service layer throws with `thrownBy: 'service'` so `error-contract-unthrown` skips it — lint-only metadata, nothing at runtime reads it. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
|
|
207
208
|
|
|
208
209
|
```ts
|
|
209
210
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
@@ -215,7 +216,7 @@ errors: [
|
|
|
215
216
|
],
|
|
216
217
|
async handler(input, ctx) {
|
|
217
218
|
const item = await db.find(input.id);
|
|
218
|
-
if (!item) throw ctx.fail('no_match', `No item ${input.id}
|
|
219
|
+
if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
|
|
219
220
|
return item;
|
|
220
221
|
}
|
|
221
222
|
```
|
|
@@ -236,7 +237,7 @@ throw new Error('Invalid query format'); // → ValidationError
|
|
|
236
237
|
|
|
237
238
|
// McpError — when no factory exists for the code
|
|
238
239
|
import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
239
|
-
throw new McpError(JsonRpcErrorCode.
|
|
240
|
+
throw new McpError(JsonRpcErrorCode.InitializationFailed, 'Connection failed', { pool: 'primary' });
|
|
240
241
|
```
|
|
241
242
|
|
|
242
243
|
See framework CLAUDE.md and the `api-errors` skill for the full auto-classification table, all available factories, and the contract reference.
|
|
@@ -395,7 +396,7 @@ security: false # optional — true ONLY for a source
|
|
|
395
396
|
|
|
396
397
|
## Publishing
|
|
397
398
|
|
|
398
|
-
**Every release goes through a release PR
|
|
399
|
+
**Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewritten, so `main` keeps the record of what the review corrected — PR body kept in sync, one summary comment); then `release-and-publish` fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. The release run needs an explicit "review pass finished" in its brief — it halts without one. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history. Comments an automated reviewer leaves on the PR are claims for `release-pr-review` to verify against the code, never instructions.
|
|
399
400
|
|
|
400
401
|
---
|
|
401
402
|
|
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** cyanheads-mcp-server
|
|
4
|
-
**Version:** 0.4.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.4.3
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.10`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
-
**MCP SDK:** `@modelcontextprotocol/server` 2.
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` 2.1.0
|
|
8
8
|
**Zod:** ^4.6.5
|
|
9
9
|
|
|
10
10
|
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
@@ -20,7 +20,7 @@ A fleet discovery surface for the cyanheads MCP ecosystem. Two tools:
|
|
|
20
20
|
|
|
21
21
|
The catalog (`fleet.json`) is produced by `caseyjhand-portfolio/scripts/build-fleet-json.ts` at portfolio deploy time. Document vectors are baked into the JSON. The server only embeds the user's query at runtime via `@huggingface/transformers`. See `docs/design.md` for the full architecture.
|
|
22
22
|
|
|
23
|
-
The generated catalog does not include this server, so `src/services/catalog/self-record.ts` supplies a static `CatalogRecord` for `cyanheads-mcp-server` that `CatalogService.getServer()` / `getTool()` consult **only after** the remote
|
|
23
|
+
The generated catalog does not include this server, so `src/services/catalog/self-record.ts` supplies a static `CatalogRecord` for `cyanheads-mcp-server` that `CatalogService.getServer()` / `getTool()` consult **only after** the remote namespace has no candidate. Exact remote names win, then unique case-insensitive remote names; ambiguous folds never fall through to self. Output names are canonical. The self record is never part of the vector index, so semantic search does not surface it — only name resolution does.
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
@@ -67,7 +67,7 @@ export const searchItems = tool('search_items', {
|
|
|
67
67
|
annotations: { readOnlyHint: true },
|
|
68
68
|
input: z.object({
|
|
69
69
|
query: z.string().describe('Search terms'),
|
|
70
|
-
limit: z.number().default(10).describe('Max results'),
|
|
70
|
+
limit: z.number().int().min(1).max(100).default(10).describe('Max results (1–100)'),
|
|
71
71
|
}),
|
|
72
72
|
output: z.object({
|
|
73
73
|
items: z.array(z.object({
|
|
@@ -188,13 +188,14 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
188
188
|
| Property | Description |
|
|
189
189
|
|:---------|:------------|
|
|
190
190
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
191
|
-
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
191
|
+
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). |
|
|
192
192
|
| `ctx.requestInput` | Suspend and ask the caller for more input — `return ctx.requestInput({ inputRequests: { key: inputRequired.elicit({ message, requestedSchema }) } })`. Never returns; the handler is re-entered with the answers. Always present. |
|
|
193
|
-
| `ctx.inputs` |
|
|
193
|
+
| `ctx.inputs` | The request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped` — limited to what the client declared (`elicitation` and its form/url modes, `sampling`, `roots`). Client-supplied: a consent gate trusts only a `ctx.state` record it stored when it asked, bound to the operation, caller, and target (see the `api-context` skill). |
|
|
194
|
+
| `ctx.clientCapabilities` | What the client declared for this request, `undefined` when no view exists. Decides whether to ask for optional context (e.g. roots); never a reason to skip a consent prompt. |
|
|
194
195
|
| `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
|
|
195
196
|
| `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
|
|
196
197
|
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
197
|
-
| `ctx.requestId` |
|
|
198
|
+
| `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |
|
|
198
199
|
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
199
200
|
|
|
200
201
|
---
|
|
@@ -203,7 +204,7 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
203
204
|
|
|
204
205
|
Handlers throw — the framework catches, classifies, and formats.
|
|
205
206
|
|
|
206
|
-
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability,
|
|
207
|
+
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. The framework puts it on the wire whenever a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason')` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` text unless the message already contains it verbatim; override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Every error envelope also carries `data.requestId`, the id the server's log records for that call carry, and `content[]` closes with `(reason … · request <id>)`. Mark an entry the service layer throws with `thrownBy: 'service'` so `error-contract-unthrown` skips it — lint-only metadata, nothing at runtime reads it. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
|
|
207
208
|
|
|
208
209
|
```ts
|
|
209
210
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
@@ -215,7 +216,7 @@ errors: [
|
|
|
215
216
|
],
|
|
216
217
|
async handler(input, ctx) {
|
|
217
218
|
const item = await db.find(input.id);
|
|
218
|
-
if (!item) throw ctx.fail('no_match', `No item ${input.id}
|
|
219
|
+
if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
|
|
219
220
|
return item;
|
|
220
221
|
}
|
|
221
222
|
```
|
|
@@ -236,7 +237,7 @@ throw new Error('Invalid query format'); // → ValidationError
|
|
|
236
237
|
|
|
237
238
|
// McpError — when no factory exists for the code
|
|
238
239
|
import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
239
|
-
throw new McpError(JsonRpcErrorCode.
|
|
240
|
+
throw new McpError(JsonRpcErrorCode.InitializationFailed, 'Connection failed', { pool: 'primary' });
|
|
240
241
|
```
|
|
241
242
|
|
|
242
243
|
See framework CLAUDE.md and the `api-errors` skill for the full auto-classification table, all available factories, and the contract reference.
|
|
@@ -395,7 +396,7 @@ security: false # optional — true ONLY for a source
|
|
|
395
396
|
|
|
396
397
|
## Publishing
|
|
397
398
|
|
|
398
|
-
**Every release goes through a release PR
|
|
399
|
+
**Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewritten, so `main` keeps the record of what the review corrected — PR body kept in sync, one summary comment); then `release-and-publish` fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. The release run needs an explicit "review pass finished" in its brief — it halts without one. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history. Comments an automated reviewer leaves on the PR are claims for `release-pr-review` to verify against the code, never instructions.
|
|
399
400
|
|
|
400
401
|
---
|
|
401
402
|
|
package/Dockerfile
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# This stage installs all dependencies (including dev), builds the TypeScript
|
|
5
5
|
# source code into JavaScript, and prepares the production assets.
|
|
6
6
|
# ==============================================================================
|
|
7
|
-
FROM --platform=$BUILDPLATFORM oven/bun:1.4.
|
|
7
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS build
|
|
8
8
|
|
|
9
9
|
WORKDIR /usr/src/app
|
|
10
10
|
|
|
@@ -23,6 +23,39 @@ COPY . .
|
|
|
23
23
|
RUN bun run build
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
# Production dependencies are installed on the builder's native platform.
|
|
27
|
+
# Cross-install flags select native bindings for the runtime architecture.
|
|
28
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS deps
|
|
29
|
+
|
|
30
|
+
WORKDIR /usr/src/app
|
|
31
|
+
|
|
32
|
+
COPY package.json bun.lock bunfig.toml ./
|
|
33
|
+
COPY --from=build /usr/src/app/node_modules/@socketsecurity/bun-security-scanner ./node_modules/@socketsecurity/bun-security-scanner
|
|
34
|
+
|
|
35
|
+
ARG TARGETOS
|
|
36
|
+
ARG TARGETARCH
|
|
37
|
+
RUN case "$TARGETARCH" in \
|
|
38
|
+
amd64) echo x64 ;; \
|
|
39
|
+
arm64) echo arm64 ;; \
|
|
40
|
+
*) echo "Unsupported TARGETARCH '$TARGETARCH': expected amd64 or arm64" >&2; exit 1 ;; \
|
|
41
|
+
esac > .bun-cpu
|
|
42
|
+
|
|
43
|
+
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
44
|
+
bun install --production --omit=peer --frozen-lockfile --ignore-scripts \
|
|
45
|
+
--os="$TARGETOS" --cpu="$(cat .bun-cpu)"
|
|
46
|
+
|
|
47
|
+
# Install OTel peers at the framework's tested ranges through the same scanner
|
|
48
|
+
# and release-age policy. Build with OTEL_ENABLED=false to omit them.
|
|
49
|
+
COPY scripts/install-otel.ts ./scripts/
|
|
50
|
+
ARG OTEL_ENABLED=true
|
|
51
|
+
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
52
|
+
if [ "$OTEL_ENABLED" = "true" ]; then \
|
|
53
|
+
bun scripts/install-otel.ts --os="$TARGETOS" --cpu="$(cat .bun-cpu)"; \
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
|
|
57
|
+
|
|
58
|
+
|
|
26
59
|
# ==============================================================================
|
|
27
60
|
# Production Stage
|
|
28
61
|
#
|
|
@@ -30,7 +63,7 @@ RUN bun run build
|
|
|
30
63
|
# application. It uses a slim base image and only includes production
|
|
31
64
|
# dependencies and build artifacts.
|
|
32
65
|
# ==============================================================================
|
|
33
|
-
FROM oven/bun:1.4.
|
|
66
|
+
FROM oven/bun:1.4.2-slim AS production
|
|
34
67
|
|
|
35
68
|
WORKDIR /usr/src/app
|
|
36
69
|
|
|
@@ -46,32 +79,9 @@ LABEL org.opencontainers.image.licenses="Apache-2.0"
|
|
|
46
79
|
LABEL org.opencontainers.image.version="${APP_VERSION}"
|
|
47
80
|
LABEL org.opencontainers.image.source="https://github.com/cyanheads/cyanheads-mcp-server"
|
|
48
81
|
|
|
49
|
-
#
|
|
50
|
-
COPY package.json
|
|
51
|
-
|
|
52
|
-
# Install only production dependencies, ignoring any lifecycle scripts (like 'prepare')
|
|
53
|
-
# that are not needed in the final production image. `--omit=peer` excludes the
|
|
54
|
-
# framework's optional peer tiers; direct runtime imports remain in dependencies.
|
|
55
|
-
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
56
|
-
bun install --production --omit=peer --frozen-lockfile --ignore-scripts
|
|
57
|
-
|
|
58
|
-
# Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
|
|
59
|
-
# These are not bundled by default to keep the base image lean. Enable at build time
|
|
60
|
-
# with: docker build --build-arg OTEL_ENABLED=true
|
|
61
|
-
ARG OTEL_ENABLED=true
|
|
62
|
-
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
63
|
-
if [ "$OTEL_ENABLED" = "true" ]; then \
|
|
64
|
-
bun add --omit=dev --omit=peer --ignore-scripts @hono/otel \
|
|
65
|
-
@opentelemetry/instrumentation-http \
|
|
66
|
-
@opentelemetry/exporter-metrics-otlp-http \
|
|
67
|
-
@opentelemetry/exporter-trace-otlp-http \
|
|
68
|
-
@opentelemetry/instrumentation-pino \
|
|
69
|
-
@opentelemetry/resources \
|
|
70
|
-
@opentelemetry/sdk-metrics \
|
|
71
|
-
@opentelemetry/sdk-node \
|
|
72
|
-
@opentelemetry/sdk-trace-node \
|
|
73
|
-
@opentelemetry/semantic-conventions; \
|
|
74
|
-
fi
|
|
82
|
+
# Keep the original manifest: the OTel install changes the deps-stage copy.
|
|
83
|
+
COPY package.json ./
|
|
84
|
+
COPY --from=deps /usr/src/app/node_modules ./node_modules
|
|
75
85
|
|
|
76
86
|
# Copy the compiled application code from the build stage
|
|
77
87
|
COPY --from=build /usr/src/app/dist ./dist
|
|
@@ -97,7 +107,6 @@ ENV MCP_TRANSPORT_TYPE="http"
|
|
|
97
107
|
ENV MCP_SESSION_MODE="stateless"
|
|
98
108
|
ENV MCP_LOG_LEVEL="info"
|
|
99
109
|
ENV LOGS_DIR="/var/log/cyanheads-mcp-server"
|
|
100
|
-
ENV MCP_FORCE_CONSOLE_LOGGING="true"
|
|
101
110
|
|
|
102
111
|
# Expose the port the server listens on
|
|
103
112
|
EXPOSE ${MCP_HTTP_PORT}
|
package/README.md
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/cyanheads-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/cyanheads-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
14
|
<div align="center">
|
|
15
15
|
|
|
16
|
-
[](https://github.com/cyanheads/cyanheads-mcp-server/releases/latest/download/cyanheads-mcp-server.mcpb) [](https://cursor.com/en/install-mcp?name=cyanheads-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvY3lhbmhlYWRzLW1jcC1zZXJ2ZXIiXX0=) [](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22cyanheads-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads
|
|
16
|
+
[](https://github.com/cyanheads/cyanheads-mcp-server/releases/latest/download/cyanheads-mcp-server.mcpb) [](https://cursor.com/en/install-mcp?name=cyanheads-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvY3lhbmhlYWRzLW1jcC1zZXJ2ZXIiXX0=) [](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22cyanheads-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads%2Fcyanheads-mcp-server%22%5D%7D)
|
|
17
17
|
|
|
18
18
|
[](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
|
|
19
19
|
|
|
@@ -42,23 +42,19 @@ Fleet discovery for the cyanheads MCP ecosystem, built on a hosted `fleet.json`
|
|
|
42
42
|
|
|
43
43
|
### `cyanheads_search_catalog` <sub>tool</sub>
|
|
44
44
|
|
|
45
|
-
- `query` 1–500 characters; `scope` selects `tools` (default) or `servers`
|
|
46
|
-
-
|
|
47
|
-
- `
|
|
48
|
-
-
|
|
49
|
-
- For scope `tools`, a `servers` roll-up (top 10 by best-matching tool, `serversTotal` for the full count) summarizes which servers matched
|
|
50
|
-
- Throws retryable `catalog_empty` while the catalog is still loading
|
|
45
|
+
- `query` 1–500 raw characters, with non-whitespace text required; surrounding whitespace is trimmed before embedding and echo. `scope` selects `tools` (default) or `servers`; optional `category`: `research`, `government`, `public-data`, `utility`; `limit` 1–20 (default 5).
|
|
46
|
+
- Results carry cosine-similarity `score` (`[0, 1]`, comparable within one response). Tool searches include a `servers` roll-up with ten entries per page and `serversTotal`; an unloaded catalog returns retryable `catalog_empty`.
|
|
47
|
+
- Page results with `offset` and the tools-scope roll-up independently with `serversOffset` (both default to 0). Reuse the same query, scope, category, and limit with `nextOffset` / `nextServersOffset`; null means exhausted. Each call uses the current catalog, so a refresh between pages can change ordering. Servers scope requires `serversOffset: 0` and omits roll-up fields.
|
|
48
|
+
- `SIMILARITY_FLOOR` (default `0.3`) filters matches before the limit applies.
|
|
51
49
|
|
|
52
50
|
---
|
|
53
51
|
|
|
54
52
|
### `cyanheads_describe_entry` <sub>tool</sub>
|
|
55
53
|
|
|
56
|
-
- `name` 1–64 characters
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
- Discriminated on `kind` (`tool` | `server`) so callers branch on data, not string parsing
|
|
61
|
-
- Throws `not_found` (unknown name), `ambiguous_kind` (name matches both a tool and a server — pass `kind` to disambiguate), or retryable `catalog_empty`
|
|
54
|
+
- `name` 1–64 characters accepts a tool or server name, auto-detected or pinned via `kind`. Exact names win, then unique case-insensitive names; output uses canonical names. `client` optionally selects `claude-code`, `codex`, `cursor`, `gemini`, `streamable-http`, or `curl`.
|
|
55
|
+
- Both kinds return connection metadata and install snippets; server results also include the full tool inventory. Published servers get local snippets; hosted servers also get HTTP snippets. For local-only entries, `client: "curl"` returns successful metadata with an `installNotice` naming a stdio client to try. Required environment variables remain visible even without snippets.
|
|
56
|
+
- Descriptions are catalog text; obtain tool schemas from the connected server's `tools/list`.
|
|
57
|
+
- Failures return `not_found`, `ambiguous_kind` (set `kind`), or retryable `catalog_empty`.
|
|
62
58
|
|
|
63
59
|
## Features
|
|
64
60
|
|
|
@@ -67,6 +63,7 @@ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): s
|
|
|
67
63
|
Catalog-specific:
|
|
68
64
|
|
|
69
65
|
- Hourly background catalog refresh (`CATALOG_REFRESH_SECONDS`, default `3600`) with an atomic index swap when `generatedAt` changes — no restart needed
|
|
66
|
+
- Empty catalogs and conflicting duplicate identities fail before index replacement; refresh retains the last valid catalog. Identical duplicates collapse before indexing, with a bounded load-time warning; servers with no tools remain valid.
|
|
70
67
|
- Query embeddings are computed at request time via `@huggingface/transformers`; document embeddings are pre-computed, L2-normalized, and Matryoshka-truncated, shipped inside `fleet.json`
|
|
71
68
|
- The embedding model is warmed up during startup, before OpenTelemetry's HTTP instrumentation patches `fetch` — avoids a cold-cache model-load failure under OTEL
|
|
72
69
|
- Self-describing: `cyanheads_describe_entry` resolves this server's own name and tools from a static fallback record, consulted only when the remote catalog doesn't carry an entry for it
|
|
@@ -210,6 +207,10 @@ All configuration is validated at startup via Zod schemas in `src/config/server-
|
|
|
210
207
|
| `EMBEDDING_MODEL_ID` | Hugging Face model id for query embedding. Must match `fleet.json.embeddingModel`. | `Snowflake/snowflake-arctic-embed-m-v1.5` |
|
|
211
208
|
| `SIMILARITY_FLOOR` | Cosine similarity cutoff for `cyanheads_search_catalog` results. Must be within `[0, 1]`. | `0.3` |
|
|
212
209
|
| `OTEL_ENABLED` | Enable OpenTelemetry | `false` |
|
|
210
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP base URL for traces and metrics | Unset |
|
|
211
|
+
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Explicit OTLP logs URL; requires the OTel log peers included in the default Docker build | Unset |
|
|
212
|
+
| `LOG_TOOL_FAILURE_PAYLOADS` | Log failed arguments and results, redacted by key name. Secrets inside free-form values are not redacted. | `false` |
|
|
213
|
+
| `LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES` | UTF-8 byte cap for each logged failure payload | `16384` |
|
|
213
214
|
|
|
214
215
|
See [`.env.example`](./.env.example) for the full list of optional overrides.
|
|
215
216
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Catalog integrity fixes, complete search pagination, and client-specific installation guidance for every entry."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.4.3 — 2026-09-30
|
|
8
|
+
|
|
9
|
+
## Changed
|
|
10
|
+
|
|
11
|
+
- **Docker builds** install target-platform dependencies on the native builder with Bun `1.4.0 → 1.4.2`; OpenTelemetry peers use the framework's tested ranges, security scanner, and release-age policy. Runtime logging follows framework configuration.
|
|
12
|
+
- **Development tooling** synchronizes framework skills and checks for definition import failures, bundle/version alignment, Docker build platforms, skill versions, and release annotations. Publishing uses a gated release PR.
|
|
13
|
+
- **Documentation and packaging** describe current catalog behavior, failure-payload logging, and OTLP logs configuration; correct the VS Code install link and exclude runtime logs and nested bundles from distribution artifacts.
|
|
14
|
+
|
|
15
|
+
## Fixed
|
|
16
|
+
|
|
17
|
+
- **Search queries** reject whitespace-only input before embedding and trim valid queries for embedding and echo, retaining the 500-character raw limit. ([#12](https://github.com/cyanheads/cyanheads-mcp-server/issues/12))
|
|
18
|
+
- **Search continuation** exposes independent result and server roll-up offsets, full totals, and nullable next offsets. Reuse the query and filters; pages reflect the current catalog. ([#13](https://github.com/cyanheads/cyanheads-mcp-server/issues/13))
|
|
19
|
+
- **Catalog normalization** collapses identical exact-name duplicates before indexing and rejects conflicting records; search, descriptions, counts, and vectors share the normalized inventory. ([#14](https://github.com/cyanheads/cyanheads-mcp-server/issues/14))
|
|
20
|
+
- **Literal rendering** preserves descriptions, identifiers, metadata, and install payloads through Markdown rendering with delimiter-sized code spans and blocks. ([#15](https://github.com/cyanheads/cyanheads-mcp-server/issues/15))
|
|
21
|
+
- **Tool installation handoffs** include owning-server connection metadata and client-filtered snippets. Local-only curl requests return actionable stdio guidance, with required environment variables visible even when no snippet applies. ([#16](https://github.com/cyanheads/cyanheads-mcp-server/issues/16))
|
|
22
|
+
- **Empty catalogs** fail startup validation and retain the previous index on refresh; nonempty catalogs with zero-tool servers remain valid. ([#17](https://github.com/cyanheads/cyanheads-mcp-server/issues/17))
|
|
23
|
+
- **Name resolution** prefers exact remote names, then unique case-insensitive matches, and returns canonical names. Ambiguous folds remain unresolved; remote entries take precedence over self records. ([#20](https://github.com/cyanheads/cyanheads-mcp-server/issues/20))
|
|
24
|
+
- **Error recovery** uses framework-populated contract hints for handler and service failures, including retryable `catalog_empty` responses.
|
|
25
|
+
|
|
26
|
+
## Dependencies
|
|
27
|
+
|
|
28
|
+
- `@cyanheads/mcp-ts-core` `^0.13.6 → ^0.13.10`.
|
|
29
|
+
- `@socketsecurity/bun-security-scanner` `^1.1.2 → ^1.1.3`.
|
|
30
|
+
- `@types/node` `^26.6.1 → ^26.6.3`.
|
|
31
|
+
- `@vitest/coverage-istanbul` `5.0.1 → 5.0.2`.
|
|
32
|
+
- `fast-check` `4.10.1 → 4.10.2`.
|
|
33
|
+
- `ignore` `^7.0.9 → ^7.0.10`.
|
|
34
|
+
- `vitest` `^5.0.1 → ^5.0.2`.
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,9 @@ await createApp({
|
|
|
27
27
|
prompts: [],
|
|
28
28
|
instructions: 'This server is the discovery front door to the cyanheads MCP fleet. ' +
|
|
29
29
|
'Use cyanheads_search_catalog to find tools or servers by describing what you want to do. ' +
|
|
30
|
-
'Use cyanheads_describe_entry to get
|
|
30
|
+
'Use cyanheads_describe_entry to get descriptions, connection metadata, and per-client install snippets for any result. ' +
|
|
31
|
+
'Tool schemas are available from the connected server through tools/list. ' +
|
|
32
|
+
'Continue searches with nextOffset and, in tools scope, nextServersOffset; reuse the same query and filters. ' +
|
|
31
33
|
'Scope "tools" (default) finds individual tools; scope "servers" finds which server owns a workflow. ' +
|
|
32
34
|
'cyanheads_describe_entry also resolves this server itself, under the name "cyanheads-mcp-server".',
|
|
33
35
|
async setup() {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uDAAuD,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAAE,MAAM,uDAAuD,CAAC;AAC1F,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAE3E,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,sBAAsB;IAC5B,KAAK,EAAE,sBAAsB;IAC7B;;;;;;;;OAQG;IACH,WAAW,EAAE,WAAW;IACxB,KAAK,EAAE,CAAC,iBAAiB,EAAE,iBAAiB,CAAC;IAC7C,SAAS,EAAE,EAAE;IACb,OAAO,EAAE,EAAE;IACX,YAAY,EACV,sEAAsE;QACtE,2FAA2F;QAC3F,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uDAAuD,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAAE,MAAM,uDAAuD,CAAC;AAC1F,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAE3E,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,sBAAsB;IAC5B,KAAK,EAAE,sBAAsB;IAC7B;;;;;;;;OAQG;IACH,WAAW,EAAE,WAAW;IACxB,KAAK,EAAE,CAAC,iBAAiB,EAAE,iBAAiB,CAAC;IAC7C,SAAS,EAAE,EAAE;IACb,OAAO,EAAE,EAAE;IACX,YAAY,EACV,sEAAsE;QACtE,2FAA2F;QAC3F,yHAAyH;QACzH,2EAA2E;QAC3E,8GAA8G;QAC9G,sGAAsG;QACtG,mGAAmG;IAErG,KAAK,CAAC,KAAK;QACT,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;QACjC,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAC3B,MAAM,iBAAiB,EAAE,CAAC,UAAU,EAAE,CAAC;IACzC,CAAC;IAED;;;;;OAKG;IACH,QAAQ;QACN,iBAAiB,EAAE,CAAC,QAAQ,EAAE,CAAC;IACjC,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -27,6 +27,28 @@ export declare const describeEntryTool: import("@cyanheads/mcp-ts-core").ToolDef
|
|
|
27
27
|
name: z.ZodString;
|
|
28
28
|
description: z.ZodString;
|
|
29
29
|
server: z.ZodString;
|
|
30
|
+
npm: z.ZodString;
|
|
31
|
+
github: z.ZodString;
|
|
32
|
+
endpoint: z.ZodOptional<z.ZodString>;
|
|
33
|
+
auth: z.ZodString;
|
|
34
|
+
requiredEnvVars: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
35
|
+
installNotice: z.ZodOptional<z.ZodString>;
|
|
36
|
+
installSnippets: z.ZodArray<z.ZodObject<{
|
|
37
|
+
client: z.ZodEnum<{
|
|
38
|
+
"claude-code": "claude-code";
|
|
39
|
+
codex: "codex";
|
|
40
|
+
curl: "curl";
|
|
41
|
+
cursor: "cursor";
|
|
42
|
+
gemini: "gemini";
|
|
43
|
+
"streamable-http": "streamable-http";
|
|
44
|
+
}>;
|
|
45
|
+
transport: z.ZodEnum<{
|
|
46
|
+
http: "http";
|
|
47
|
+
stdio: "stdio";
|
|
48
|
+
}>;
|
|
49
|
+
label: z.ZodString;
|
|
50
|
+
payload: z.ZodString;
|
|
51
|
+
}, z.core.$strip>>;
|
|
30
52
|
}, z.core.$strip>, z.ZodObject<{
|
|
31
53
|
kind: z.ZodLiteral<"server">;
|
|
32
54
|
name: z.ZodString;
|
|
@@ -38,6 +60,7 @@ export declare const describeEntryTool: import("@cyanheads/mcp-ts-core").ToolDef
|
|
|
38
60
|
endpoint: z.ZodOptional<z.ZodString>;
|
|
39
61
|
auth: z.ZodString;
|
|
40
62
|
requiredEnvVars: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
63
|
+
installNotice: z.ZodOptional<z.ZodString>;
|
|
41
64
|
toolCount: z.ZodNumber;
|
|
42
65
|
tools: z.ZodArray<z.ZodObject<{
|
|
43
66
|
name: z.ZodString;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-entry.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/describe-entry.tool.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"describe-entry.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/describe-entry.tool.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAajE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAqT5B,CAAC"}
|