@openbiox/bizard-mcp 0.0.0-stage → 0.1.1
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 +91 -2
- package/dist/adapt-tools.d.ts +3 -0
- package/dist/adapt-tools.js +45 -0
- package/dist/adapt.d.ts +12 -0
- package/dist/adapt.js +77 -0
- package/dist/bundle.d.ts +4 -0
- package/dist/bundle.js +35 -0
- package/dist/citation.d.ts +6 -0
- package/dist/citation.js +34 -0
- package/dist/contribute-tools.d.ts +3 -0
- package/dist/contribute-tools.js +41 -0
- package/dist/contribute.d.ts +44 -0
- package/dist/contribute.js +136 -0
- package/dist/entry.d.ts +17 -0
- package/dist/entry.js +48 -0
- package/dist/http.d.ts +9 -0
- package/dist/http.js +109 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +74 -0
- package/dist/resources.d.ts +3 -0
- package/dist/resources.js +36 -0
- package/dist/results.d.ts +13 -0
- package/dist/results.js +6 -0
- package/dist/search.d.ts +21 -0
- package/dist/search.js +64 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.js +115 -0
- package/dist/snapshot/bundle.json +170377 -0
- package/dist/snapshot/llms.txt +892 -0
- package/dist/tutorial-tools.d.ts +10 -0
- package/dist/tutorial-tools.js +38 -0
- package/dist/tutorial.d.ts +31 -0
- package/dist/tutorial.js +53 -0
- package/dist/types.d.ts +49 -0
- package/dist/types.js +1 -0
- package/dist/update-check.d.ts +24 -0
- package/dist/update-check.js +49 -0
- package/dist/usage.d.ts +10 -0
- package/dist/usage.js +44 -0
- package/package.json +37 -4
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { Bundle } from "./types.js";
|
|
3
|
+
import type { UsageEvent } from "./usage.js";
|
|
4
|
+
/** Lazy bundle access so degraded mode (null snapshot) surfaces as a tool error, not a crash. */
|
|
5
|
+
export interface LazyBundle {
|
|
6
|
+
get(): Bundle;
|
|
7
|
+
}
|
|
8
|
+
/** Fire-and-forget usage tracker (local anonymous JSONL, README Privacy). */
|
|
9
|
+
export type UsageTracker = (ev: UsageEvent) => void;
|
|
10
|
+
export declare function registerTutorialTools(server: McpServer, bundle: LazyBundle, track: UsageTracker): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { tutorialPayload, codePayload } from "./tutorial.js";
|
|
3
|
+
import { jsonResult, errorResult } from "./results.js";
|
|
4
|
+
export function registerTutorialTools(server, bundle, track) {
|
|
5
|
+
server.registerTool("bizard_get_tutorial", {
|
|
6
|
+
title: "Get tutorial structure",
|
|
7
|
+
description: "Structured tutorial content: dependencies, example data URLs, languages available, and tutorial link.",
|
|
8
|
+
inputSchema: { chartId: z.string() },
|
|
9
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
10
|
+
}, async ({ chartId }) => {
|
|
11
|
+
try {
|
|
12
|
+
const payload = tutorialPayload(bundle.get(), chartId);
|
|
13
|
+
track({ event: "view", chartId });
|
|
14
|
+
return jsonResult(payload);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
return errorResult(e.message);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
server.registerTool("bizard_get_code", {
|
|
21
|
+
title: "Get runnable chart code",
|
|
22
|
+
description: "Runnable code for one chart in R, Python, or Julia with package list and example-data URLs.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
chartId: z.string(),
|
|
25
|
+
language: z.enum(["r", "python", "julia"]),
|
|
26
|
+
},
|
|
27
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
28
|
+
}, async ({ chartId, language }) => {
|
|
29
|
+
try {
|
|
30
|
+
const payload = codePayload(bundle.get(), chartId, language);
|
|
31
|
+
track({ event: "view", chartId, language });
|
|
32
|
+
return jsonResult(payload);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
return errorResult(e.message);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Bundle, Chart, CodeSnippet } from "./types.js";
|
|
2
|
+
export declare function findChart(bundle: Bundle, id: string): Chart | undefined;
|
|
3
|
+
export declare function snippetsFor(bundle: Bundle, chartId: string): CodeSnippet[];
|
|
4
|
+
export declare function tutorialPayload(bundle: Bundle, chartId: string): {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
nameZh: string | null;
|
|
8
|
+
description: string;
|
|
9
|
+
tutorialUrl: string;
|
|
10
|
+
tutorialUrlZh: string | null;
|
|
11
|
+
imageUrl: string;
|
|
12
|
+
packages: string[];
|
|
13
|
+
dataUrls: string[];
|
|
14
|
+
languages: string[];
|
|
15
|
+
snippetCount: number;
|
|
16
|
+
};
|
|
17
|
+
export declare function codePayload(bundle: Bundle, chartId: string, language: "r" | "python" | "julia"): {
|
|
18
|
+
code: null;
|
|
19
|
+
packages: never[];
|
|
20
|
+
dataUrls: never[];
|
|
21
|
+
license: string;
|
|
22
|
+
message: string;
|
|
23
|
+
tutorialUrl: string;
|
|
24
|
+
} | {
|
|
25
|
+
code: string;
|
|
26
|
+
packages: string[];
|
|
27
|
+
dataUrls: string[];
|
|
28
|
+
license: string;
|
|
29
|
+
message: null;
|
|
30
|
+
tutorialUrl?: undefined;
|
|
31
|
+
};
|
package/dist/tutorial.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const LICENSE_NOTE = "CC-BY-NC-4.0: non-commercial use with attribution. Cite Bizard (see bizard_cite).";
|
|
2
|
+
export function findChart(bundle, id) {
|
|
3
|
+
const lower = id.toLowerCase();
|
|
4
|
+
return bundle.charts.find((c) => c.id.toLowerCase() === lower);
|
|
5
|
+
}
|
|
6
|
+
export function snippetsFor(bundle, chartId) {
|
|
7
|
+
return bundle.codeSnippets.filter((s) => s.chartId === chartId);
|
|
8
|
+
}
|
|
9
|
+
export function tutorialPayload(bundle, chartId) {
|
|
10
|
+
const chart = findChart(bundle, chartId);
|
|
11
|
+
if (!chart)
|
|
12
|
+
throw new Error(`Unknown chartId: ${chartId}`);
|
|
13
|
+
const snips = snippetsFor(bundle, chart.id);
|
|
14
|
+
return {
|
|
15
|
+
id: chart.id,
|
|
16
|
+
name: chart.name,
|
|
17
|
+
nameZh: chart.nameZh,
|
|
18
|
+
description: chart.description,
|
|
19
|
+
tutorialUrl: chart.tutorialUrl,
|
|
20
|
+
tutorialUrlZh: chart.tutorialUrlZh,
|
|
21
|
+
imageUrl: chart.imageUrl,
|
|
22
|
+
packages: [...new Set(snips.flatMap((s) => s.packages))],
|
|
23
|
+
dataUrls: [...new Set(snips.flatMap((s) => s.dataUrls))],
|
|
24
|
+
languages: chart.codeRefs,
|
|
25
|
+
snippetCount: snips.length,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function codePayload(bundle, chartId, language) {
|
|
29
|
+
const chart = findChart(bundle, chartId);
|
|
30
|
+
if (!chart)
|
|
31
|
+
throw new Error(`Unknown chartId: ${chartId}`);
|
|
32
|
+
const snips = snippetsFor(bundle, chart.id).filter((s) => s.lang === language);
|
|
33
|
+
if (chart.codeRefs.length === 0) {
|
|
34
|
+
return {
|
|
35
|
+
code: null,
|
|
36
|
+
packages: [],
|
|
37
|
+
dataUrls: [],
|
|
38
|
+
license: LICENSE_NOTE,
|
|
39
|
+
message: `This tutorial has no structured code. Full tutorial: ${chart.tutorialUrl}`,
|
|
40
|
+
tutorialUrl: chart.tutorialUrl,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (snips.length === 0) {
|
|
44
|
+
throw new Error(`No ${language} code for ${chartId}. Available languages: ${chart.codeRefs.join(", ")}.`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
code: snips.map((s) => s.code).join("\n\n"),
|
|
48
|
+
packages: [...new Set(snips.flatMap((s) => s.packages))],
|
|
49
|
+
dataUrls: [...new Set(snips.flatMap((s) => s.dataUrls))],
|
|
50
|
+
license: LICENSE_NOTE,
|
|
51
|
+
message: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface BundleMeta {
|
|
2
|
+
siteVersion: string;
|
|
3
|
+
bundleVersion: number;
|
|
4
|
+
generatedAt: string;
|
|
5
|
+
license: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Category {
|
|
8
|
+
name: string;
|
|
9
|
+
nameZh: string | null;
|
|
10
|
+
count: number;
|
|
11
|
+
}
|
|
12
|
+
export interface Chart {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
nameZh: string | null;
|
|
16
|
+
type: string;
|
|
17
|
+
level1: string;
|
|
18
|
+
level2: string;
|
|
19
|
+
tutorialUrl: string;
|
|
20
|
+
tutorialUrlZh: string | null;
|
|
21
|
+
imageUrl: string;
|
|
22
|
+
description: string;
|
|
23
|
+
codeRefs: string[];
|
|
24
|
+
}
|
|
25
|
+
export interface CodeSnippet {
|
|
26
|
+
chartId: string;
|
|
27
|
+
lang: "r" | "python" | "julia";
|
|
28
|
+
code: string;
|
|
29
|
+
packages: string[];
|
|
30
|
+
dataUrls: string[];
|
|
31
|
+
sourceAnchor: string;
|
|
32
|
+
}
|
|
33
|
+
export interface Citation {
|
|
34
|
+
paperDoi: string;
|
|
35
|
+
paperUrl: string;
|
|
36
|
+
bibtex: string;
|
|
37
|
+
license: string;
|
|
38
|
+
}
|
|
39
|
+
export interface Bundle {
|
|
40
|
+
meta: BundleMeta;
|
|
41
|
+
categories: Category[];
|
|
42
|
+
charts: Chart[];
|
|
43
|
+
codeSnippets: CodeSnippet[];
|
|
44
|
+
keywords: {
|
|
45
|
+
synonyms: Record<string, string[]>;
|
|
46
|
+
intentKeywords: Record<string, string[]>;
|
|
47
|
+
} | null;
|
|
48
|
+
citation: Citation;
|
|
49
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Bundle } from "./types.js";
|
|
2
|
+
/** CI publishes the bundle here (mcp-bundle.yml → files/mcp/bundle.json on main). */
|
|
3
|
+
export declare const REMOTE_BUNDLE_URL = "https://openbiox.github.io/Bizard/files/mcp/bundle.json";
|
|
4
|
+
/** Mutable cell the server reads per-call; the update check hot-swaps its contents. */
|
|
5
|
+
export interface MutableHolder {
|
|
6
|
+
bundle: Bundle | null;
|
|
7
|
+
etag: string | null;
|
|
8
|
+
}
|
|
9
|
+
export type NoticeQueue = string[];
|
|
10
|
+
export interface UpdateCheckDeps {
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
onSwap?: (bundle: Bundle, etag: string) => void;
|
|
13
|
+
notices: NoticeQueue;
|
|
14
|
+
stderr?: (line: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function createNoticeQueue(): NoticeQueue;
|
|
17
|
+
export declare function updateNotice(notices: NoticeQueue): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Spec §3.4: background HEAD of the remote bundle comparing ETag; on change, GET +
|
|
20
|
+
* validate + hot-swap, queueing a ONE-TIME notice for the session's next tool result.
|
|
21
|
+
* Offline / network / validation failure: silent fallback to the in-memory snapshot,
|
|
22
|
+
* one stderr line, never throws.
|
|
23
|
+
*/
|
|
24
|
+
export declare function checkForUpdate(holder: MutableHolder, deps: UpdateCheckDeps): Promise<boolean>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { loadBundle } from "./bundle.js";
|
|
2
|
+
/** CI publishes the bundle here (mcp-bundle.yml → files/mcp/bundle.json on main). */
|
|
3
|
+
export const REMOTE_BUNDLE_URL = "https://openbiox.github.io/Bizard/files/mcp/bundle.json";
|
|
4
|
+
function diffCounts(oldB, newB) {
|
|
5
|
+
if (!oldB)
|
|
6
|
+
return { added: newB.charts.length, updated: 0 };
|
|
7
|
+
const oldIds = new Set(oldB.charts.map((c) => c.id));
|
|
8
|
+
let shared = 0;
|
|
9
|
+
for (const c of newB.charts)
|
|
10
|
+
if (oldIds.has(c.id))
|
|
11
|
+
shared += 1;
|
|
12
|
+
return { added: newB.charts.length - shared, updated: shared };
|
|
13
|
+
}
|
|
14
|
+
export function createNoticeQueue() {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
export function updateNotice(notices) {
|
|
18
|
+
return notices.shift() ?? null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Spec §3.4: background HEAD of the remote bundle comparing ETag; on change, GET +
|
|
22
|
+
* validate + hot-swap, queueing a ONE-TIME notice for the session's next tool result.
|
|
23
|
+
* Offline / network / validation failure: silent fallback to the in-memory snapshot,
|
|
24
|
+
* one stderr line, never throws.
|
|
25
|
+
*/
|
|
26
|
+
export async function checkForUpdate(holder, deps) {
|
|
27
|
+
const doFetch = deps.fetch ?? globalThis.fetch;
|
|
28
|
+
const stderr = deps.stderr ?? ((line) => console.error(line));
|
|
29
|
+
try {
|
|
30
|
+
const head = await doFetch(REMOTE_BUNDLE_URL, { method: "HEAD" });
|
|
31
|
+
const etag = head.headers.get("ETag");
|
|
32
|
+
if (!etag || etag === holder.etag)
|
|
33
|
+
return false;
|
|
34
|
+
const res = await doFetch(REMOTE_BUNDLE_URL);
|
|
35
|
+
if (!res.ok)
|
|
36
|
+
return false;
|
|
37
|
+
const remote = loadBundle(await res.json());
|
|
38
|
+
const { added, updated } = diffCounts(holder.bundle, remote);
|
|
39
|
+
holder.bundle = remote;
|
|
40
|
+
holder.etag = etag;
|
|
41
|
+
deps.onSwap?.(remote, etag);
|
|
42
|
+
deps.notices.push(`[Bizard 内容包已更新至 v${remote.meta.bundleVersion}:新增 ${added} 图、更新 ${updated} 图]`);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
stderr(`bizard-mcp: update check failed, using built-in snapshot (${e.message})`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface UsageEvent {
|
|
2
|
+
event: "search" | "view" | "adapt" | "cite";
|
|
3
|
+
chartId?: string;
|
|
4
|
+
language?: string;
|
|
5
|
+
query?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Append one anonymized usage row (query hashed, never stored verbatim). Returns false when logging is off. */
|
|
8
|
+
export declare function recordUsage(ev: UsageEvent, overridePath?: string): Promise<boolean>;
|
|
9
|
+
/** Read the last `limit` usage rows; a missing file yields []. */
|
|
10
|
+
export declare function readUsage(limit?: number, overridePath?: string): Promise<Array<Record<string, unknown>>>;
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
const DEFAULT_LOG = join(homedir(), ".cache", "bizard-mcp", "usage-log.jsonl");
|
|
6
|
+
/** Resolve the active log path: BIZARD_USAGE_LOG wins ("off" disables), else the explicit override, else the default. */
|
|
7
|
+
function logPath(override) {
|
|
8
|
+
const env = process.env.BIZARD_USAGE_LOG;
|
|
9
|
+
if (env !== undefined && env !== "off")
|
|
10
|
+
return env;
|
|
11
|
+
return override ?? DEFAULT_LOG;
|
|
12
|
+
}
|
|
13
|
+
function sha256_8(s) {
|
|
14
|
+
return createHash("sha256").update(s).digest("hex").slice(0, 8);
|
|
15
|
+
}
|
|
16
|
+
/** Append one anonymized usage row (query hashed, never stored verbatim). Returns false when logging is off. */
|
|
17
|
+
export async function recordUsage(ev, overridePath) {
|
|
18
|
+
if (process.env.BIZARD_USAGE_LOG === "off")
|
|
19
|
+
return false;
|
|
20
|
+
const target = logPath(overridePath);
|
|
21
|
+
const row = {
|
|
22
|
+
ts: new Date().toISOString(),
|
|
23
|
+
event: ev.event,
|
|
24
|
+
chartId: ev.chartId ?? null,
|
|
25
|
+
language: ev.language ?? null,
|
|
26
|
+
queryHash: ev.query ? sha256_8(ev.query) : null,
|
|
27
|
+
};
|
|
28
|
+
await mkdir(dirname(target), { recursive: true });
|
|
29
|
+
await appendFile(target, JSON.stringify(row) + "\n", "utf-8");
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
/** Read the last `limit` usage rows; a missing file yields []. */
|
|
33
|
+
export async function readUsage(limit = 100, overridePath) {
|
|
34
|
+
if (process.env.BIZARD_USAGE_LOG === "off")
|
|
35
|
+
return [];
|
|
36
|
+
try {
|
|
37
|
+
const txt = await readFile(logPath(overridePath), "utf-8");
|
|
38
|
+
return txt.trim().split("\n").filter(Boolean).slice(-limit)
|
|
39
|
+
.map((l) => JSON.parse(l));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openbiox/bizard-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
}
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "MCP server for the Bizard biomedical visualization atlas: search, adapt, cite, and contribute chart tutorials",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": { "node": ">=20" },
|
|
7
|
+
"bin": { "bizard-mcp": "dist/index.js" },
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/openbiox/Bizard.git",
|
|
12
|
+
"directory": "mcp"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"registry": "https://registry.npmjs.org",
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc && cp -r src/snapshot dist/snapshot",
|
|
24
|
+
"build:bundle": "python3 ../.github/scripts/build-bundle.py --out src/snapshot",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest",
|
|
27
|
+
"coverage": "vitest run --coverage"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
31
|
+
"zod": "^3.24.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20.0.0",
|
|
35
|
+
"@vitest/coverage-v8": "^2.0.0",
|
|
36
|
+
"typescript": "^5.5.0",
|
|
37
|
+
"vitest": "^2.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|