@jinshuju/mcp 0.0.0-stage → 0.3.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.
@@ -0,0 +1,12 @@
1
+ export { createApp, MCP_PATH, type AppOptions } from './app.js';
2
+ export { McpAnalytics, analyticsFromConfig, CONTEXT_DESCRIPTION, CONTEXT_INSTRUCTIONS, type AnalyticsOptions } from './analytics.js';
3
+ export { buildServer, INSTRUCTIONS, META, type BuildServerOptions } from './server.js';
4
+ export { TOOLS, SCHEMAS, toToolResult } from './tools.js';
5
+ export { OPERATIONS, runComposite } from './composites.js';
6
+ export { JinshujuApi, buildUrl, buildBody, bodyPayload, TransportError, type ApiResponse } from './api.js';
7
+ export { configFromEnv, DEFAULTS, type Config } from './config.js';
8
+ export { StaticCredential, MissingCredentialError, type Credential } from './credential.js';
9
+ export { CliSessionCredential, resolveCredential, DEFAULT_CONFIG_PATH } from './cli-credential.js';
10
+ export { TokenVerifier, bearerToken, challenge, loadAuthServerMetadata, sha256 } from './auth.js';
11
+ export type { ToolSpec, OperationSpec, ParamSpec, BodySpec, GeneratedMeta, JsonSchema } from './types.js';
12
+ export { VERSION, SERVER_NAME, SERVER_TITLE } from './version.js';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ export { createApp, MCP_PATH } from './app.js';
2
+ export { McpAnalytics, analyticsFromConfig, CONTEXT_DESCRIPTION, CONTEXT_INSTRUCTIONS } from './analytics.js';
3
+ export { buildServer, INSTRUCTIONS, META } from './server.js';
4
+ export { TOOLS, SCHEMAS, toToolResult } from './tools.js';
5
+ export { OPERATIONS, runComposite } from './composites.js';
6
+ export { JinshujuApi, buildUrl, buildBody, bodyPayload, TransportError } from './api.js';
7
+ export { configFromEnv, DEFAULTS } from './config.js';
8
+ export { StaticCredential, MissingCredentialError } from './credential.js';
9
+ export { CliSessionCredential, resolveCredential, DEFAULT_CONFIG_PATH } from './cli-credential.js';
10
+ export { TokenVerifier, bearerToken, challenge, loadAuthServerMetadata, sha256 } from './auth.js';
11
+ export { VERSION, SERVER_NAME, SERVER_TITLE } from './version.js';
12
+ //# sourceMappingURL=index.js.map
package/dist/node.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/node.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ /** Node process entry, used by the official deployment (Docker / k8s). */
3
+ import { serve } from '@hono/node-server';
4
+ import { analyticsFromConfig } from './analytics.js';
5
+ import { createApp } from './app.js';
6
+ import { configFromEnv } from './config.js';
7
+ import { VERSION } from './version.js';
8
+ const port = Number(process.env.PORT ?? 8787);
9
+ const host = process.env.HOST ?? '0.0.0.0';
10
+ const config = configFromEnv();
11
+ const analytics = analyticsFromConfig(config);
12
+ const app = createApp({ config, analytics });
13
+ serve({ fetch: app.fetch, port, hostname: host }, (info) => {
14
+ console.error(`jinshuju-mcp ${VERSION} listening on http://${info.address}:${info.port}/mcp`);
15
+ });
16
+ // posthog-node sends in timed batches; flush the queued events before the process exits.
17
+ if (analytics) {
18
+ const stop = () => {
19
+ analytics
20
+ .shutdown()
21
+ .catch((error) => console.error('posthog shutdown failed:', error))
22
+ .finally(() => process.exit(0));
23
+ };
24
+ process.once('SIGTERM', stop);
25
+ process.once('SIGINT', stop);
26
+ }
27
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Turns an API response into a CallToolResult the model can read: compact JSON on success (truncated when needed),
3
+ * and on failure the HTTP status + the server's error_description / errors[] + a line on what to do.
4
+ */
5
+ import type { CallToolResult } from '@modelcontextprotocol/server';
6
+ import type { ApiResponse } from './api.js';
7
+ import type { ToolSpec } from './types.js';
8
+ /** The 403 hint; it names the scope itself, so analytics.ts leaves it out when looking for a scope failure in the API's own words. */
9
+ export declare const FORBIDDEN_HINT = "No permission, a quota is used up, or the OAuth scope is insufficient.";
10
+ export declare function toToolResult(tool: ToolSpec, res: ApiResponse, maxResultChars: number): CallToolResult;
package/dist/result.js ADDED
@@ -0,0 +1,81 @@
1
+ function errorLines(res) {
2
+ const lines = [];
3
+ const body = res.json;
4
+ const stated = body?.error_description ?? body?.message ?? body?.error;
5
+ if (typeof stated === 'string' && stated.trim())
6
+ lines.push(stated.trim());
7
+ const errors = body?.errors;
8
+ if (Array.isArray(errors)) {
9
+ for (const item of errors) {
10
+ if (typeof item === 'string')
11
+ lines.push(`- ${item}`);
12
+ else if (item && typeof item === 'object') {
13
+ const { pointer, message, code, index, reason } = item;
14
+ const where = pointer ?? (index === undefined ? undefined : `row ${index}`);
15
+ const what = message ?? reason ?? JSON.stringify(item);
16
+ lines.push(`- ${where === undefined ? '' : `${where}: `}${what}${code ? ` (${code})` : ''}`);
17
+ }
18
+ }
19
+ }
20
+ if (!lines.length && res.text)
21
+ lines.push(res.text);
22
+ return lines;
23
+ }
24
+ /** The 403 hint; it names the scope itself, so analytics.ts leaves it out when looking for a scope failure in the API's own words. */
25
+ export const FORBIDDEN_HINT = 'No permission, a quota is used up, or the OAuth scope is insufficient.';
26
+ function hint(status, res) {
27
+ switch (status) {
28
+ case 400:
29
+ return 'Arguments or data failed validation: fix them as described above and retry.';
30
+ case 401:
31
+ return 'The credential is invalid or expired: re-authorize, or use another access token.';
32
+ case 402:
33
+ return 'The current plan does not include API v1.';
34
+ case 403:
35
+ return FORBIDDEN_HINT;
36
+ case 404:
37
+ return 'The resource does not exist or this credential cannot access it (check the token / serial number).';
38
+ case 415:
39
+ return 'The request body must be JSON.';
40
+ case 422:
41
+ return 'Rejected; see the reasons above. A batch returns 422 only when every item failed.';
42
+ case 429:
43
+ return `Rate limit exceeded; retry ${res.retryAfter ? `in ${res.retryAfter} seconds` : 'later'}.`;
44
+ default:
45
+ return status >= 500 ? 'Jinshuju server error; retry later, and report it if it keeps happening.' : '';
46
+ }
47
+ }
48
+ export function toToolResult(tool, res, maxResultChars) {
49
+ const note = res.warnings ? `\n⚠ Server warnings about the arguments (X-API-Input-Warnings): ${res.warnings}` : '';
50
+ if (res.status >= 200 && res.status < 300) {
51
+ const payload = res.json;
52
+ let text = payload === undefined ? (res.text ?? '') : JSON.stringify(payload);
53
+ let truncated = false;
54
+ if (text.length > maxResultChars) {
55
+ truncated = true;
56
+ text =
57
+ `${text.slice(0, maxResultChars)}\n… (Result truncated: ${text.length} characters in all. ` +
58
+ 'Narrow it with fields / limit / filters / keyword, or page through it with next.)';
59
+ }
60
+ const result = { content: [{ type: 'text', text: text + note }] };
61
+ if (tool.outputSchema) {
62
+ // A tool that declares an outputSchema must return structuredContent. When truncated, don't stuff the whole large result in again; return a small explanatory object.
63
+ if (truncated) {
64
+ result.structuredContent = { truncated: true, total_chars: JSON.stringify(payload).length };
65
+ }
66
+ else if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
67
+ result.structuredContent = payload;
68
+ }
69
+ else {
70
+ result.structuredContent = { result: payload };
71
+ }
72
+ }
73
+ return result;
74
+ }
75
+ const lines = [`HTTP ${res.status}: ${tool.name} failed.`, ...errorLines(res)];
76
+ const h = hint(res.status, res);
77
+ if (h)
78
+ lines.push(h);
79
+ return { isError: true, content: [{ type: 'text', text: lines.join('\n') + note }] };
80
+ }
81
+ //# sourceMappingURL=result.js.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Assembles an McpServer. Stateless: one per HTTP request / per stdio connection, cheap and independent of each other.
3
+ */
4
+ import { McpServer } from '@modelcontextprotocol/server';
5
+ import type { Config } from './config.js';
6
+ import type { Credential } from './credential.js';
7
+ import { type ToolRuntime } from './tools.js';
8
+ import type { GeneratedMeta } from './types.js';
9
+ export declare const META: GeneratedMeta;
10
+ export declare const INSTRUCTIONS: string;
11
+ export interface BuildServerOptions {
12
+ credential: Credential;
13
+ config?: Partial<Config>;
14
+ fetch?: typeof fetch;
15
+ /** Server instructions, INSTRUCTIONS by default; with intent collection on in HTTP mode, the context argument's note is appended */
16
+ instructions?: string;
17
+ /** Callback when a tool throws (the HTTP deployment reports to Sentry); the error still goes to the model as usual */
18
+ onToolError?: ToolRuntime['onError'];
19
+ }
20
+ export declare function buildServer({ credential, config, fetch: fetchImpl, instructions, onToolError }: BuildServerOptions): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Assembles an McpServer. Stateless: one per HTTP request / per stdio connection, cheap and independent of each other.
3
+ */
4
+ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/server';
5
+ import { JinshujuApi } from './api.js';
6
+ import { DEFAULTS } from './config.js';
7
+ import { registerGeneratedTools, registerSchemaTools, SCHEMAS } from './tools.js';
8
+ import { SERVER_NAME, SERVER_TITLE, VERSION } from './version.js';
9
+ import metaJson from './generated/meta.json' with { type: 'json' };
10
+ export const META = metaJson;
11
+ export const INSTRUCTIONS = [
12
+ 'MCP server for Jinshuju (金数据) forms, tables and entries. Each tool maps to the open API v1 and is generated from its openapi.yaml.',
13
+ "Usual path: list_forms finds the form token → get_form shows the fields' api_code → list_entries / count_entries / aggregate_entries read entries;",
14
+ 'create_entry / patch_entries write them, keyed by api_code.',
15
+ 'To create a form: get_schema("FieldInput") lists the field types, get_schema("<Type>Input") shows a type\'s properties, then call create_form.',
16
+ 'Wherever an input says `Shape: schema "X"`, look X up with get_schema; do not guess.',
17
+ "An error gives the HTTP status and the server's error_description / errors; fix the call as they say and retry. On 429, wait as Retry-After says.",
18
+ 'A personal token sees only the forms its user created or that are shared with them; an account token sees every form in the account.'
19
+ ].join('\n');
20
+ export function buildServer({ credential, config, fetch: fetchImpl, instructions, onToolError }) {
21
+ const cfg = { ...DEFAULTS, ...config };
22
+ const api = new JinshujuApi({
23
+ baseUrl: cfg.apiBaseUrl,
24
+ fetch: fetchImpl,
25
+ timeoutMs: cfg.timeoutMs,
26
+ userAgent: `jinshuju-mcp/${VERSION}`
27
+ });
28
+ const server = new McpServer({ name: SERVER_NAME, title: SERVER_TITLE, version: VERSION, websiteUrl: 'https://open.jinshuju.net/mcp' }, { instructions: instructions ?? INSTRUCTIONS });
29
+ registerGeneratedTools(server, { api, credential, maxResultChars: cfg.maxResultChars, onError: onToolError });
30
+ registerSchemaTools(server, META.pointerSchemas);
31
+ // The same schema catalog is also exposed as resources, for clients that prefer resources.
32
+ server.registerResource('schema', new ResourceTemplate('jinshuju://schemas/{name}', {
33
+ list: () => ({
34
+ resources: META.pointerSchemas.map((name) => ({
35
+ uri: `jinshuju://schemas/${name}`,
36
+ name,
37
+ title: `schema ${name}`,
38
+ mimeType: 'application/schema+json',
39
+ description: SCHEMAS[name]?.description?.split('\n')[0]
40
+ }))
41
+ }),
42
+ complete: {
43
+ name: (value) => Object.keys(SCHEMAS)
44
+ .filter((n) => n.toLowerCase().startsWith(value.toLowerCase()))
45
+ .slice(0, 20)
46
+ }
47
+ }), {
48
+ title: 'API v1 schemas',
49
+ description: 'JSON Schemas from components.schemas of openapi.yaml, read by name.',
50
+ mimeType: 'application/schema+json'
51
+ }, async (uri, { name }) => {
52
+ const schema = SCHEMAS[String(name)];
53
+ if (!schema)
54
+ throw new Error(`No schema named ${String(name)}`);
55
+ return { contents: [{ uri: uri.href, mimeType: 'application/schema+json', text: JSON.stringify(schema) }] };
56
+ });
57
+ return server;
58
+ }
59
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/stdio.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Local stdio entry: `claude mcp add jinshuju -- npx -y @jinshuju/mcp`.
4
+ * The credential comes from JINSHUJU_ACCESS_TOKEN, or from ~/.jinshuju/config.json after logging in with @jinshuju/cli.
5
+ */
6
+ import { serveStdio } from '@modelcontextprotocol/server/stdio';
7
+ import { configFromEnv } from './config.js';
8
+ import { resolveCredential } from './cli-credential.js';
9
+ import { MissingCredentialError } from './credential.js';
10
+ import { buildServer } from './server.js';
11
+ import { VERSION } from './version.js';
12
+ if (process.argv.includes('--version') || process.argv.includes('-V')) {
13
+ console.log(VERSION);
14
+ process.exit(0);
15
+ }
16
+ try {
17
+ const credential = await resolveCredential();
18
+ const config = configFromEnv();
19
+ console.error(`jinshuju-mcp ${VERSION}: ${credential.describe()}`);
20
+ serveStdio(() => buildServer({ credential, config }));
21
+ }
22
+ catch (error) {
23
+ if (error instanceof MissingCredentialError) {
24
+ console.error(error.message);
25
+ process.exit(3);
26
+ }
27
+ throw error;
28
+ }
29
+ //# sourceMappingURL=stdio.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Registers the generated ToolSpecs as MCP tools: a one-to-one tool sends its request directly; a composite tool goes to composites.ts.
3
+ */
4
+ import { type McpServer } from '@modelcontextprotocol/server';
5
+ import type { JinshujuApi } from './api.js';
6
+ import type { Credential } from './credential.js';
7
+ import type { ToolSpec, JsonSchema } from './types.js';
8
+ export { toToolResult } from './result.js';
9
+ export declare const TOOLS: ToolSpec[];
10
+ export declare const SCHEMAS: Record<string, JsonSchema>;
11
+ export interface ToolRuntime {
12
+ api: JinshujuApi;
13
+ credential: Credential;
14
+ maxResultChars: number;
15
+ /** Told first when a tool throws (the HTTP deployment reports to Sentry); the error still goes to the model as usual; stdio passes none */
16
+ onError?: (error: unknown, tool: string) => void;
17
+ }
18
+ export declare function registerGeneratedTools(server: McpServer, runtime: ToolRuntime): void;
19
+ /** Lists the schema names that can be looked up, for get_schema's error and the resources list. */
20
+ export declare function schemaNames(): string[];
21
+ export declare function registerSchemaTools(server: McpServer, pointerSchemas: string[]): void;
package/dist/tools.js ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Registers the generated ToolSpecs as MCP tools: a one-to-one tool sends its request directly; a composite tool goes to composites.ts.
3
+ */
4
+ import { fromJsonSchema } from '@modelcontextprotocol/server';
5
+ import { TransportError } from './api.js';
6
+ import { runComposite } from './composites.js';
7
+ import { toToolResult } from './result.js';
8
+ import { toolInputValidator } from './validation.js';
9
+ import toolsJson from './generated/tools.json' with { type: 'json' };
10
+ import schemasJson from './generated/schemas.json' with { type: 'json' };
11
+ export { toToolResult } from './result.js';
12
+ export const TOOLS = toolsJson;
13
+ export const SCHEMAS = schemasJson;
14
+ /**
15
+ * outputSchema only tells the model what comes back; it never rejects a response:
16
+ * when openapi.yaml and the live implementation disagree now and then, a successful API call must not turn into a tool error.
17
+ */
18
+ const permissiveValidator = {
19
+ getValidator: () => (input) => ({ valid: true, data: input, errorMessage: undefined })
20
+ };
21
+ export function registerGeneratedTools(server, runtime) {
22
+ for (const tool of TOOLS) {
23
+ server.registerTool(tool.name, {
24
+ title: tool.title,
25
+ description: tool.description,
26
+ inputSchema: fromJsonSchema(tool.inputSchema, toolInputValidator),
27
+ outputSchema: tool.outputSchema ? fromJsonSchema(tool.outputSchema, permissiveValidator) : undefined,
28
+ annotations: tool.annotations
29
+ }, async (args) => {
30
+ try {
31
+ if (tool.composite)
32
+ return await runComposite(tool, args ?? {}, runtime);
33
+ const res = await runtime.api.call(tool, args ?? {}, runtime.credential);
34
+ return toToolResult(tool, res, runtime.maxResultChars);
35
+ }
36
+ catch (error) {
37
+ runtime.onError?.(error, tool.name);
38
+ if (error instanceof TransportError) {
39
+ return { isError: true, content: [{ type: 'text', text: `${error.message}. Retry later.` }] };
40
+ }
41
+ throw error;
42
+ }
43
+ });
44
+ }
45
+ }
46
+ /** Lists the schema names that can be looked up, for get_schema's error and the resources list. */
47
+ export function schemaNames() {
48
+ return Object.keys(SCHEMAS);
49
+ }
50
+ export function registerSchemaTools(server, pointerSchemas) {
51
+ server.registerTool('get_schema', {
52
+ title: 'Show a schema',
53
+ description: 'Returns the full JSON Schema of a schema in openapi.yaml. Where a tool input or result says `Shape: schema "X"` or ' +
54
+ '`Returns schema "X"`, look X up here; a schema that branches on type (such as FieldInput) carries x-variants, ' +
55
+ `which names the schema of each branch. Common ones: ${pointerSchemas.slice(0, 6).join(', ')}.`,
56
+ inputSchema: fromJsonSchema({
57
+ type: 'object',
58
+ properties: {
59
+ name: { type: 'string', description: 'Schema name, e.g. FieldInput, TextFieldInput, FormSettingWriteInput' }
60
+ },
61
+ required: ['name'],
62
+ additionalProperties: false
63
+ }, toolInputValidator),
64
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
65
+ }, async ({ name }) => {
66
+ const schema = SCHEMAS[name];
67
+ if (!schema) {
68
+ const candidates = schemaNames()
69
+ .filter((n) => n.toLowerCase().includes(name.toLowerCase()))
70
+ .slice(0, 10);
71
+ return {
72
+ isError: true,
73
+ content: [
74
+ {
75
+ type: 'text',
76
+ text: `No schema named ${name}.${candidates.length ? ` Similar names: ${candidates.join(', ')}` : ''}`
77
+ }
78
+ ]
79
+ };
80
+ }
81
+ return { content: [{ type: 'text', text: JSON.stringify(schema) }] };
82
+ });
83
+ }
84
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The shape of the generated output (src/generated/*.json). A change here needs the same change in scripts/generate.ts.
3
+ */
4
+ export type JsonSchema = Record<string, unknown>;
5
+ export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
6
+ export interface ParamSpec {
7
+ name: string;
8
+ in: 'path' | 'query';
9
+ required: boolean;
10
+ /** A query array is serialized as name[]=a&name[]=b (the server reads it as an array) */
11
+ array?: boolean;
12
+ /** Declared with `content: application/json`: sent as one query value holding JSON text (a string is passed through as is) */
13
+ json?: boolean;
14
+ }
15
+ export interface BodySpec {
16
+ contentType: 'application/json' | 'multipart/form-data';
17
+ required: boolean;
18
+ /** Body properties sit at the top level of the arguments: the keys left after removing path / query parameters are the body */
19
+ flatten: boolean;
20
+ /** When not flattened, the argument key that holds the body */
21
+ param?: string;
22
+ /** Wrapped once more as { [wrap]: payload } before sending (for projection tools) */
23
+ wrap?: string;
24
+ }
25
+ /** One openapi operation: everything needed to send its request at runtime */
26
+ export interface OperationSpec {
27
+ operationId: string;
28
+ summary: string;
29
+ description: string;
30
+ tag: string;
31
+ method: HttpMethod;
32
+ path: string;
33
+ params: ParamSpec[];
34
+ paramNames: string[];
35
+ body?: BodySpec;
36
+ successStatus: number;
37
+ scopes: string[];
38
+ responseSchema?: string;
39
+ }
40
+ export interface ToolAnnotations {
41
+ readOnlyHint?: boolean;
42
+ destructiveHint?: boolean;
43
+ idempotentHint?: boolean;
44
+ openWorldHint?: boolean;
45
+ }
46
+ export type CompositeKind = 'scene_form_create' | 'scene_form_update' | 'count_entries';
47
+ /** One MCP tool: an operation + input / output schemas; a composite tool also carries composite */
48
+ export interface ToolSpec extends OperationSpec {
49
+ name: string;
50
+ title: string;
51
+ inputSchema: JsonSchema;
52
+ outputSchema?: JsonSchema;
53
+ annotations: ToolAnnotations;
54
+ composite?: {
55
+ kind: CompositeKind;
56
+ scene?: string;
57
+ settingOperation?: string;
58
+ settingKey?: string;
59
+ };
60
+ }
61
+ export interface GeneratedMeta {
62
+ generatedFrom: {
63
+ title: string;
64
+ version: string;
65
+ sha256: string;
66
+ };
67
+ baseUrl: string;
68
+ toolCount: number;
69
+ operationCount: number;
70
+ /** Tool name -> operationId (a+b for a composite tool) */
71
+ tools: Record<string, string>;
72
+ pointerSchemas: string[];
73
+ omitSchemas: string[];
74
+ tags: Record<string, string>;
75
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Tool argument validator: behaves the same on Node and Workers, and its errors name the argument at fault.
3
+ *
4
+ * On Node the SDK uses Ajv by default, which reports an extra key only as "data must NOT have additional properties"
5
+ * without naming it, leaving the model to guess and drop keys one at a time. This uses the pure-JS @cfworker/json-schema
6
+ * engine everywhere (it is what Workers uses anyway), and checks for extra top-level keys before the engine, listing the
7
+ * extra and the accepted argument names directly.
8
+ */
9
+ import type { jsonSchemaValidator } from '@modelcontextprotocol/server';
10
+ /**
11
+ * The intent argument PostHog MCP analytics (analytics.ts) adds to every tool. The model fills it from the tool list, which
12
+ * may be cached: clients keep sending it after analytics is turned off, so validation always accepts and strips it, on or
13
+ * off, and it never reaches the API.
14
+ */
15
+ export declare const CONTEXT_ARGUMENT = "context";
16
+ export declare const toolInputValidator: jsonSchemaValidator;
@@ -0,0 +1,56 @@
1
+ import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker';
2
+ const engine = new CfWorkerJsonSchemaValidator({ shortcircuit: false });
3
+ /**
4
+ * The intent argument PostHog MCP analytics (analytics.ts) adds to every tool. The model fills it from the tool list, which
5
+ * may be cached: clients keep sending it after analytics is turned off, so validation always accepts and strips it, on or
6
+ * off, and it never reaches the API.
7
+ */
8
+ export const CONTEXT_ARGUMENT = 'context';
9
+ function isRecord(value) {
10
+ return !!value && typeof value === 'object' && !Array.isArray(value);
11
+ }
12
+ /**
13
+ * Engine errors look like `#/limit: Instance type "string" is invalid. Expected "integer".`; this turns the JSON pointer
14
+ * into an argument path, and drops the vague outer errors the engine adds after a sub-schema fails ("Property x does not
15
+ * match schema" and the like).
16
+ */
17
+ const NOISE = [/^Property "[^"]+" does not match (additional properties )?schema\.$/, /^False boolean schema\.$/];
18
+ function readable(message) {
19
+ if (!message)
20
+ return 'Invalid arguments';
21
+ const lines = message
22
+ .split('; ')
23
+ .map((line) => line.replace(/^#\/?([^:]*):\s*/, (_m, path) => (path ? `${path.replace(/\//g, '.')}: ` : '')));
24
+ const specific = lines.filter((line) => !NOISE.some((re) => re.test(line.replace(/^[^:]*: /, ''))));
25
+ return [...new Set(specific.length > 0 ? specific : lines)].join('; ');
26
+ }
27
+ export const toolInputValidator = {
28
+ getValidator(schema) {
29
+ const validate = engine.getValidator(schema);
30
+ const known = Object.keys(schema.properties ?? {});
31
+ const strict = schema.additionalProperties === false;
32
+ const stripContext = !known.includes(CONTEXT_ARGUMENT);
33
+ return (raw) => {
34
+ let input = raw;
35
+ if (stripContext && isRecord(raw) && CONTEXT_ARGUMENT in raw) {
36
+ const { [CONTEXT_ARGUMENT]: _context, ...rest } = raw;
37
+ input = rest;
38
+ }
39
+ if (strict && isRecord(input)) {
40
+ const extra = Object.keys(input).filter((key) => !known.includes(key));
41
+ if (extra.length > 0) {
42
+ return {
43
+ valid: false,
44
+ data: undefined,
45
+ errorMessage: `Unknown arguments: ${extra.join(', ')}. This tool accepts only: ${known.join(', ')}`
46
+ };
47
+ }
48
+ }
49
+ const result = validate(input);
50
+ if (result.valid)
51
+ return result;
52
+ return { valid: false, data: undefined, errorMessage: readable(result.errorMessage) };
53
+ };
54
+ }
55
+ };
56
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1,3 @@
1
+ export declare const VERSION = "0.3.0";
2
+ export declare const SERVER_NAME = "jinshuju";
3
+ export declare const SERVER_TITLE = "Jinshuju MCP Server";
@@ -0,0 +1,5 @@
1
+ // release-please keeps this in sync through extra-files; the runtime does not read package.json because Workers has no file system.
2
+ export const VERSION = '0.3.0'; // x-release-please-version
3
+ export const SERVER_NAME = 'jinshuju';
4
+ export const SERVER_TITLE = 'Jinshuju MCP Server';
5
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Cloudflare Workers entry (an optional deployment). No Durable Objects / KV: the server is fully stateless.
3
+ * With SENTRY_DSN set, exceptions go to Sentry (the SDK reads SENTRY_DSN / SENTRY_ENVIRONMENT from env itself); without it,
4
+ * nothing is sent.
5
+ */
6
+ import { type CloudflareOptions } from '@sentry/cloudflare';
7
+ type Env = Record<string, string | undefined>;
8
+ /** Errors only, no tracing; no request bodies / IPs / cookies: tool arguments and results hold user data, same policy as analytics.ts. */
9
+ export declare function sentryOptions(): CloudflareOptions;
10
+ declare const _default: {
11
+ fetch(request: Request, env: Env, ctx: unknown): Promise<Response> | Response;
12
+ };
13
+ export default _default;
package/dist/worker.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Cloudflare Workers entry (an optional deployment). No Durable Objects / KV: the server is fully stateless.
3
+ * With SENTRY_DSN set, exceptions go to Sentry (the SDK reads SENTRY_DSN / SENTRY_ENVIRONMENT from env itself); without it,
4
+ * nothing is sent.
5
+ */
6
+ import { withSentry } from '@sentry/cloudflare';
7
+ import { createApp } from './app.js';
8
+ import { configFromEnv } from './config.js';
9
+ import { VERSION } from './version.js';
10
+ let app;
11
+ /** Errors only, no tracing; no request bodies / IPs / cookies: tool arguments and results hold user data, same policy as analytics.ts. */
12
+ export function sentryOptions() {
13
+ return {
14
+ // Releases are organization-wide in Sentry, so the worker name keeps them apart from goldendata's; the name cannot contain /, so the npm package name won't do
15
+ release: `jinshuju-mcp@${VERSION}`,
16
+ dataCollection: { httpBodies: [], userInfo: false, cookies: false },
17
+ // Events are sent from the server, so the IP Sentry infers from the connection is this service's, not the user's; the javascript platform infers it by default, so turn it off explicitly
18
+ beforeSend(event) {
19
+ event.sdk = { ...event.sdk, settings: { ...event.sdk?.settings, infer_ip: 'never' } };
20
+ return event;
21
+ }
22
+ };
23
+ }
24
+ export default withSentry(sentryOptions, {
25
+ fetch(request, env, ctx) {
26
+ app ??= createApp({ config: configFromEnv(env) });
27
+ return app.fetch(request, env, ctx);
28
+ }
29
+ });
30
+ //# sourceMappingURL=worker.js.map