canli-validation-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # canli-validation-mcp
2
+
3
+ An MCP (Model Context Protocol) server over canlicapital.com's free, keyed validation API. It
4
+ gives a coding agent seven tools: issue a free key, run the four validators (deflated Sharpe,
5
+ CSCV overfitting, paper-evidence conformance, breadth ceiling), fetch a stored receipt, and read
6
+ service status. Every tool returns the full API envelope as its result text, success or error,
7
+ so the agent cannot see a number without the sentences beside it that say what the number does
8
+ not establish.
9
+
10
+ This package is not published to npm. Run it from a local checkout, as shown below.
11
+
12
+ ## What the API is (and is not)
13
+
14
+ The engine is the product. The service runs your submitted numbers through the same honesty
15
+ arithmetic canlicapital.com's own paper record runs on itself and hands back a verdict anyone can
16
+ recompute from the receipt. It does not accept market data, does not sign receipts, does not
17
+ grade a strategy, and never saw your data source, its costs, or any lookahead in how a series was
18
+ built. See `docs/superpowers/specs/2026-09-05-developer-key-validation-api-design.md` in the main
19
+ repository for the full design.
20
+
21
+ ## Tools
22
+
23
+ | Tool | Calls | Key required |
24
+ |---|---|---|
25
+ | `get_key` | `POST /api/v1/keys` | no |
26
+ | `validate_deflated_sharpe` | `POST /api/v1/validate/deflated-sharpe` | yes |
27
+ | `validate_overfitting` | `POST /api/v1/validate/overfitting` | yes |
28
+ | `validate_paper_evidence` | `POST /api/v1/validate/paper-evidence` | yes |
29
+ | `validate_breadth` | `POST /api/v1/validate/breadth` | yes |
30
+ | `get_receipt` | `GET /api/v1/receipts/{id}` | no |
31
+ | `service_status` | `GET /api/v1/validate/status` | no |
32
+
33
+ `validate_deflated_sharpe` accepts exactly one of two input shapes, never a mix of both:
34
+
35
+ - the seven contract fields: `observed_sharpe_annualized`, `observations`, `periods_per_year`,
36
+ `skew`, `non_excess_kurtosis`, `effective_independent_trials`,
37
+ `cross_trial_sharpe_sd_annualized`;
38
+ - or a return series plus the trials behind it: `returns`, `periods_per_year`,
39
+ `effective_independent_trials`, `cross_trial_sharpe_sd_annualized`.
40
+
41
+ Sending fields from both shapes, or from neither, is rejected before any request leaves the
42
+ process; see `src/schemas.mjs`.
43
+
44
+ ## Configuration
45
+
46
+ | Variable | Default | Meaning |
47
+ |---|---|---|
48
+ | `CANLI_API_BASE` | `https://canlicapital.com` | Where the API lives. Point it at a preview deployment for testing. |
49
+ | `CANLI_KEY` | unset | A key already issued from `POST /api/v1/keys`. When set, `get_key` sends no request and reports the key is already configured; every other tool sends it as `Authorization: Bearer <key>`. |
50
+
51
+ If `CANLI_KEY` is not set, call `get_key` once per session before the four validators. The key it
52
+ returns lives only in this process's memory for the life of the session; it is not written to
53
+ disk.
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ cd mcp
59
+ npm ci
60
+ ```
61
+
62
+ ## Claude Desktop
63
+
64
+ Add to `claude_desktop_config.json` (Settings, Developer, Edit Config):
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "canlicapital-validation": {
70
+ "command": "node",
71
+ "args": ["/absolute/path/to/meridian/mcp/src/server.mjs"],
72
+ "env": {
73
+ "CANLI_API_BASE": "https://canlicapital.com"
74
+ }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ Replace `/absolute/path/to/meridian` with the real path to this checkout, then restart Claude
81
+ Desktop.
82
+
83
+ ## Claude Code
84
+
85
+ ```bash
86
+ claude mcp add canlicapital-validation \
87
+ --env CANLI_API_BASE=https://canlicapital.com \
88
+ -- node /absolute/path/to/meridian/mcp/src/server.mjs
89
+ ```
90
+
91
+ Run `claude mcp list` to confirm it is registered, and `claude mcp remove canlicapital-validation`
92
+ to remove it.
93
+
94
+ ## Generic stdio client
95
+
96
+ Any MCP client that can spawn a process and speak stdio will work. Using the official SDK
97
+ directly, from Node:
98
+
99
+ ```js
100
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
101
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
102
+
103
+ const transport = new StdioClientTransport({
104
+ command: "node",
105
+ args: ["/absolute/path/to/meridian/mcp/src/server.mjs"],
106
+ env: { ...process.env, CANLI_API_BASE: "https://canlicapital.com" },
107
+ });
108
+
109
+ const client = new Client({ name: "my-agent", version: "0.1.0" });
110
+ await client.connect(transport);
111
+
112
+ const { tools } = await client.listTools();
113
+ console.log(tools.map((t) => t.name));
114
+
115
+ const keyResult = await client.callTool({ name: "get_key", arguments: { label: "my-agent" } });
116
+ console.log(keyResult.content[0].text); // the full envelope, key included
117
+
118
+ const result = await client.callTool({
119
+ name: "validate_deflated_sharpe",
120
+ arguments: {
121
+ returns: [0.004, -0.002, 0.007, 0.001, -0.003, 0.005, 0.002, -0.001],
122
+ periods_per_year: 252,
123
+ effective_independent_trials: 30,
124
+ cross_trial_sharpe_sd_annualized: 0.5,
125
+ },
126
+ });
127
+ console.log(result.content[0].text); // the full envelope, including limits and receipt.url
128
+
129
+ await client.close();
130
+ ```
131
+
132
+ ## What a result does not establish (boundary language)
133
+
134
+ Every envelope this server returns carries these sentences, verbatim, from the API itself
135
+ (`api/_lib/limits.js`):
136
+
137
+ - This verdict is about the series exactly as submitted. The service never saw the data source,
138
+ its costs, survivorship, or any lookahead in how the series was built.
139
+ - A deflated Sharpe or overfitting probability above or below any threshold is not admission to
140
+ anything and is not a forecast.
141
+ - The receipt is content-hashed and reproducible from the open-source core it names. It is not
142
+ signed.
143
+ - Quotas: 1000 validations per key per UTC day, 5 keys per client per UTC day, 1048576 bytes per
144
+ request, 20000 observations per series, 200 variants per matrix.
145
+
146
+ Each tool's description also states one of these sentences, so an agent sees the boundary before
147
+ it calls the tool, not only after. No tool in this server strips `limits` or `receipt.url` from
148
+ a response; the full envelope is always the result text.
149
+
150
+ ## Testing
151
+
152
+ ```bash
153
+ npm test
154
+ ```
155
+
156
+ Runs `node --test` over `test/*.test.mjs`: schema round-trips against the API's own OpenAPI and
157
+ manifest examples, one success, error-envelope and quota-429 case per keyed tool (a fake `fetch`
158
+ stands in for the network), a check that every tool description carries a limits sentence, a
159
+ check that those sentences have not drifted from `api/_lib/limits.js`, a check that no shipped
160
+ file contains an em dash, and one test that spawns the actual server binary and performs a real
161
+ MCP `tools/list` and `callTool` handshake over stdio against a local HTTP stub, so the wiring is
162
+ proven rather than assumed.
163
+
164
+ ## Dependencies
165
+
166
+ Only `@modelcontextprotocol/sdk` (pinned exact) and `zod` (pinned exact). No other runtime
167
+ dependency is added, and nothing in this package touches the site's root `package.json`,
168
+ `.vercelignore`, `api/`, `scripts/`, `js/`, or `public/`.
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "canli-validation-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for canlicapital.com's free validation API: deflated Sharpe, CSCV overfitting, paper-evidence conformance, and breadth ceiling, each returned as the full API envelope so the boundary language cannot be dropped.",
5
+ "private": false,
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "bin": {
9
+ "canlicapital-validation-mcp": "src/server.mjs"
10
+ },
11
+ "engines": {
12
+ "node": ">=20"
13
+ },
14
+ "files": [
15
+ "src",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "test": "node --test test/*.test.mjs"
20
+ },
21
+ "dependencies": {
22
+ "@modelcontextprotocol/sdk": "1.30.0",
23
+ "zod": "4.5.4"
24
+ }
25
+ }
@@ -0,0 +1,137 @@
1
+ // mcp/src/schemas.mjs
2
+ //
3
+ // Input schemas for canlicapital.com's free validation API, kept in one module so a test can
4
+ // round-trip them against the API's own examples: public/api/v1/openapi.json, the manifest at
5
+ // api/_lib/manifest.js, and the seven-field contract at
6
+ // public/glassbox/deflated_sharpe_calculator_contract.json. See
7
+ // docs/superpowers/specs/2026-09-05-developer-key-validation-api-design.md section 4 for the
8
+ // validator semantics these shapes mirror.
9
+ import { z } from "zod";
10
+
11
+ // ---------------------------------------------------------------------------------------------
12
+ // validate_deflated_sharpe: the API accepts exactly one of two input modes, never a mix.
13
+ // Input A is the seven fields of deflated_sharpe_calculator_contract.json.
14
+ // Input B is a raw return series plus the trials and dispersion behind it.
15
+ // ---------------------------------------------------------------------------------------------
16
+
17
+ export const deflatedSharpeContractInputs = z
18
+ .object({
19
+ observed_sharpe_annualized: z.number().min(-10).max(10),
20
+ observations: z.number().int().min(2).max(1000000),
21
+ periods_per_year: z.number().min(1).max(10000),
22
+ skew: z.number().min(-20).max(20),
23
+ non_excess_kurtosis: z.number().min(1).max(100),
24
+ effective_independent_trials: z.number().int().min(2).max(10000000),
25
+ cross_trial_sharpe_sd_annualized: z.number().min(0).max(10),
26
+ })
27
+ .strict();
28
+
29
+ export const deflatedSharpeReturnSeriesInputs = z
30
+ .object({
31
+ returns: z.array(z.number()).min(2).max(20000),
32
+ periods_per_year: z.number().min(1).max(10000),
33
+ effective_independent_trials: z.number().int().min(2).max(10000000),
34
+ cross_trial_sharpe_sd_annualized: z.number().min(0).max(10),
35
+ })
36
+ .strict();
37
+
38
+ // The oneOf: exactly one of the two shapes above. A value carrying fields from both, or neither,
39
+ // fails every branch and so fails the union, which is the rejection this module is tested for.
40
+ export const deflatedSharpeInput = z.union([deflatedSharpeContractInputs, deflatedSharpeReturnSeriesInputs]);
41
+
42
+ // The MCP-facing shape: every field from both modes, all optional. A bare union has no useful
43
+ // JSON Schema for a client to read, so the tool advertises this flat shape (for discovery and
44
+ // per-field type checking) while the strict oneOf rule above is enforced in the tool handler.
45
+ export const deflatedSharpeToolShape = z
46
+ .object({
47
+ observed_sharpe_annualized: z.number().min(-10).max(10).optional(),
48
+ observations: z.number().int().min(2).max(1000000).optional(),
49
+ periods_per_year: z.number().min(1).max(10000).optional(),
50
+ skew: z.number().min(-20).max(20).optional(),
51
+ non_excess_kurtosis: z.number().min(1).max(100).optional(),
52
+ effective_independent_trials: z.number().int().min(2).max(10000000).optional(),
53
+ cross_trial_sharpe_sd_annualized: z.number().min(0).max(10).optional(),
54
+ returns: z.array(z.number()).min(2).max(20000).optional(),
55
+ })
56
+ .strict();
57
+
58
+ // ---------------------------------------------------------------------------------------------
59
+ // validate_overfitting
60
+ // ---------------------------------------------------------------------------------------------
61
+
62
+ export const overfittingInput = z
63
+ .object({
64
+ matrix: z.array(z.array(z.number())).min(2).max(20000),
65
+ n_splits: z.number().int().positive().optional(),
66
+ max_combinations: z.number().int().positive().max(2000).optional(),
67
+ seed: z.number().optional(),
68
+ })
69
+ .strict();
70
+
71
+ // ---------------------------------------------------------------------------------------------
72
+ // validate_paper_evidence: the record shape is the open standard itself, so this schema only
73
+ // checks that a record object was sent; canlicapital.com runs the real conformance check.
74
+ // ---------------------------------------------------------------------------------------------
75
+
76
+ export const paperEvidenceInput = z
77
+ .object({
78
+ record: z.record(z.string(), z.unknown()),
79
+ })
80
+ .strict();
81
+
82
+ // ---------------------------------------------------------------------------------------------
83
+ // validate_breadth
84
+ // ---------------------------------------------------------------------------------------------
85
+
86
+ export const breadthInput = z
87
+ .object({
88
+ sleeve_sharpe: z.number().positive(),
89
+ average_pairwise_correlation: z.number().min(-1).max(1),
90
+ sleeves: z.number().int().min(1).max(500).optional(),
91
+ target: z.number().positive().optional(),
92
+ })
93
+ .strict();
94
+
95
+ // ---------------------------------------------------------------------------------------------
96
+ // get_key / get_receipt
97
+ // ---------------------------------------------------------------------------------------------
98
+
99
+ export const getKeyInput = z
100
+ .object({
101
+ label: z.string().max(64).optional(),
102
+ })
103
+ .strict();
104
+
105
+ export const getReceiptInput = z
106
+ .object({
107
+ id: z.string().regex(/^[0-9a-f]{24}$/, "A receipt id is 24 hex characters"),
108
+ })
109
+ .strict();
110
+
111
+ export const emptyInput = z.object({}).strict();
112
+
113
+ // ---------------------------------------------------------------------------------------------
114
+ // Boundary language: verbatim sentences from api/_lib/limits.js LIMITS_TEXT. A test asserts each
115
+ // of these strings is still present, byte for byte, in that source array, so this module cannot
116
+ // drift from the text the live API actually attaches to every envelope.
117
+ // ---------------------------------------------------------------------------------------------
118
+
119
+ export const LIMITS_SENTENCES = Object.freeze({
120
+ scope: "This verdict is about the series exactly as submitted. The service never saw the data source, its costs, survivorship, or any lookahead in how the series was built.",
121
+ notAdmission: "A deflated Sharpe or overfitting probability above or below any threshold is not admission to anything and is not a forecast.",
122
+ unsigned: "The receipt is content-hashed and reproducible from the open-source core it names. It is not signed.",
123
+ quotas: "Quotas: 1000 validations per key per UTC day, 5 keys per client per UTC day, 1048576 bytes per request, 20000 observations per series, 200 variants per matrix.",
124
+ });
125
+
126
+ // One tool description per tool, each stating (in one sentence lifted from the boundary language
127
+ // above) what the tool's result cannot be used to claim, so an agent sees this before it ever
128
+ // calls the tool, not only inside the returned envelope.
129
+ export const TOOL_DESCRIPTIONS = Object.freeze({
130
+ get_key: `Issue a free canlicapital.com validation key (POST /api/v1/keys) and hold it in memory for this session; skipped when CANLI_KEY is already set. ${LIMITS_SENTENCES.quotas}`,
131
+ validate_deflated_sharpe: `Probabilistic and deflated Sharpe from the seven contract inputs, or from a return series plus the trials and dispersion behind it, never both. ${LIMITS_SENTENCES.notAdmission}`,
132
+ validate_overfitting: `Probability of backtest overfitting by CSCV over the returns of every variant tried. ${LIMITS_SENTENCES.notAdmission}`,
133
+ validate_paper_evidence: `Conformance of a performance record against the canli.paper-evidence.v0 standard. ${LIMITS_SENTENCES.scope}`,
134
+ validate_breadth: `Book Sharpe ceiling from per-sleeve quality and average pairwise correlation, and the sleeves a target needs. ${LIMITS_SENTENCES.scope}`,
135
+ get_receipt: `Fetch a stored verdict by its content-hash id (GET /api/v1/receipts/{id}), immutable and cacheable. ${LIMITS_SENTENCES.unsigned}`,
136
+ service_status: `Service, store and quota constants for the validation API (GET /api/v1/validate/status); no key required. ${LIMITS_SENTENCES.scope}`,
137
+ });
package/src/server.mjs ADDED
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ // mcp/src/server.mjs
3
+ //
4
+ // stdio MCP server over canlicapital.com's free validation API. Every tool returns the FULL API
5
+ // envelope as its result text, success or error, so an agent cannot see a number without the
6
+ // sentences beside it that say what the number does not establish (see schemas.mjs, LIMITS_SENTENCES
7
+ // and TOOL_DESCRIPTIONS). Reads CANLI_API_BASE (default https://canlicapital.com) and an optional
8
+ // CANLI_KEY; when CANLI_KEY is set, get_key does not call the network.
9
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
+ import {
12
+ breadthInput,
13
+ deflatedSharpeInput,
14
+ deflatedSharpeToolShape,
15
+ emptyInput,
16
+ getKeyInput,
17
+ getReceiptInput,
18
+ overfittingInput,
19
+ paperEvidenceInput,
20
+ TOOL_DESCRIPTIONS,
21
+ } from "./schemas.mjs";
22
+
23
+ export const DEFAULT_BASE = "https://canlicapital.com";
24
+ export const SERVER_NAME = "canlicapital-validation-mcp";
25
+ export const SERVER_VERSION = "0.1.0";
26
+
27
+ // One place a value fails a zod schema turns into a short, readable message instead of a raw
28
+ // ZodError, so a thrown error reads well inside an MCP isError result.
29
+ function parseOrThrow(schema, value, label) {
30
+ const result = schema.safeParse(value ?? {});
31
+ if (result.success) return result.data;
32
+ const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
33
+ throw new Error(`${label}: ${issues}`);
34
+ }
35
+
36
+ export function createSession({ base, fetchImpl, envKey } = {}) {
37
+ return {
38
+ base: base ?? process.env.CANLI_API_BASE ?? DEFAULT_BASE,
39
+ fetchImpl: fetchImpl ?? fetch,
40
+ envKey: envKey ?? process.env.CANLI_KEY ?? undefined,
41
+ key: undefined,
42
+ };
43
+ }
44
+
45
+ async function callApi(session, { path, method = "GET", body }) {
46
+ const headers = { "Content-Type": "application/json" };
47
+ const key = session.key ?? session.envKey;
48
+ if (key) headers.Authorization = `Bearer ${key}`;
49
+ const init = { method, headers };
50
+ if (body !== undefined) init.body = JSON.stringify(body);
51
+ const res = await session.fetchImpl(`${session.base}${path}`, init);
52
+ const text = await res.text();
53
+ try {
54
+ return JSON.parse(text);
55
+ } catch {
56
+ throw new Error(`${path} returned a non-JSON body (status ${res.status}): ${text.slice(0, 300)}`);
57
+ }
58
+ }
59
+
60
+ const asText = (envelope) => ({ content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }] });
61
+
62
+ export async function toolGetKey(session, args) {
63
+ const { label } = parseOrThrow(getKeyInput, args, "get_key");
64
+ if (session.envKey) {
65
+ return asText({
66
+ note: "CANLI_KEY is set in the environment; no new key was issued and no request was sent. Using the configured key for this session.",
67
+ key_source: "CANLI_KEY",
68
+ key_present: true,
69
+ });
70
+ }
71
+ const envelope = await callApi(session, { path: "/api/v1/keys", method: "POST", body: { label } });
72
+ if (envelope?.data?.key) session.key = envelope.data.key;
73
+ return asText(envelope);
74
+ }
75
+
76
+ export async function toolValidateDeflatedSharpe(session, args) {
77
+ const parsed = deflatedSharpeInput.safeParse(args ?? {});
78
+ if (!parsed.success) {
79
+ throw new Error(
80
+ "Send either the seven contract inputs (observed_sharpe_annualized, observations, periods_per_year, skew, non_excess_kurtosis, effective_independent_trials, cross_trial_sharpe_sd_annualized) " +
81
+ "or a return series (returns, periods_per_year, effective_independent_trials, cross_trial_sharpe_sd_annualized), never a mix of both and never neither.",
82
+ );
83
+ }
84
+ const envelope = await callApi(session, { path: "/api/v1/validate/deflated-sharpe", method: "POST", body: parsed.data });
85
+ return asText(envelope);
86
+ }
87
+
88
+ export async function toolValidateOverfitting(session, args) {
89
+ const body = parseOrThrow(overfittingInput, args, "validate_overfitting");
90
+ const envelope = await callApi(session, { path: "/api/v1/validate/overfitting", method: "POST", body });
91
+ return asText(envelope);
92
+ }
93
+
94
+ export async function toolValidatePaperEvidence(session, args) {
95
+ const body = parseOrThrow(paperEvidenceInput, args, "validate_paper_evidence");
96
+ const envelope = await callApi(session, { path: "/api/v1/validate/paper-evidence", method: "POST", body });
97
+ return asText(envelope);
98
+ }
99
+
100
+ export async function toolValidateBreadth(session, args) {
101
+ const body = parseOrThrow(breadthInput, args, "validate_breadth");
102
+ const envelope = await callApi(session, { path: "/api/v1/validate/breadth", method: "POST", body });
103
+ return asText(envelope);
104
+ }
105
+
106
+ export async function toolGetReceipt(session, args) {
107
+ const { id } = parseOrThrow(getReceiptInput, args, "get_receipt");
108
+ const envelope = await callApi(session, { path: `/api/v1/receipts/${id}` });
109
+ return asText(envelope);
110
+ }
111
+
112
+ export async function toolServiceStatus(session) {
113
+ const envelope = await callApi(session, { path: "/api/v1/validate/status" });
114
+ return asText(envelope);
115
+ }
116
+
117
+ export function registerTools(server, session) {
118
+ server.registerTool(
119
+ "get_key",
120
+ { title: "Get a free validation key", description: TOOL_DESCRIPTIONS.get_key, inputSchema: getKeyInput },
121
+ (args) => toolGetKey(session, args),
122
+ );
123
+ server.registerTool(
124
+ "validate_deflated_sharpe",
125
+ { title: "Validate deflated Sharpe", description: TOOL_DESCRIPTIONS.validate_deflated_sharpe, inputSchema: deflatedSharpeToolShape },
126
+ (args) => toolValidateDeflatedSharpe(session, args),
127
+ );
128
+ server.registerTool(
129
+ "validate_overfitting",
130
+ { title: "Validate overfitting (CSCV)", description: TOOL_DESCRIPTIONS.validate_overfitting, inputSchema: overfittingInput },
131
+ (args) => toolValidateOverfitting(session, args),
132
+ );
133
+ server.registerTool(
134
+ "validate_paper_evidence",
135
+ { title: "Validate paper evidence", description: TOOL_DESCRIPTIONS.validate_paper_evidence, inputSchema: paperEvidenceInput },
136
+ (args) => toolValidatePaperEvidence(session, args),
137
+ );
138
+ server.registerTool(
139
+ "validate_breadth",
140
+ { title: "Validate breadth ceiling", description: TOOL_DESCRIPTIONS.validate_breadth, inputSchema: breadthInput },
141
+ (args) => toolValidateBreadth(session, args),
142
+ );
143
+ server.registerTool(
144
+ "get_receipt",
145
+ { title: "Get a receipt", description: TOOL_DESCRIPTIONS.get_receipt, inputSchema: getReceiptInput },
146
+ (args) => toolGetReceipt(session, args),
147
+ );
148
+ server.registerTool(
149
+ "service_status",
150
+ { title: "Service status", description: TOOL_DESCRIPTIONS.service_status, inputSchema: emptyInput },
151
+ () => toolServiceStatus(session),
152
+ );
153
+ }
154
+
155
+ export function createServer(session = createSession()) {
156
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
157
+ registerTools(server, session);
158
+ return server;
159
+ }
160
+
161
+ async function main() {
162
+ const server = createServer();
163
+ const transport = new StdioServerTransport();
164
+ await server.connect(transport);
165
+ }
166
+
167
+ const isMain = process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
168
+ if (isMain) {
169
+ main().catch((err) => {
170
+ console.error(`${SERVER_NAME}: fatal`, err);
171
+ process.exit(1);
172
+ });
173
+ }