@wyrdsekai/researchzosho-mcp 0.1.6-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 +31 -0
- package/bin/index.js +191 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @wyrdsekai/researchzosho-mcp
|
|
2
|
+
|
|
3
|
+
Starts [ResearchZosho](https://researchzosho.org), the research librarian for the rest of us, as an MCP server
|
|
4
|
+
over stdio. Any MCP client that can run `npx` can run it:
|
|
5
|
+
|
|
6
|
+
```json
|
|
7
|
+
{ "mcpServers": { "librarian": { "command": "npx", "args": ["-y", "@wyrdsekai/researchzosho-mcp"] } } }
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
claude mcp add --scope user librarian -- npx -y @wyrdsekai/researchzosho-mcp
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
There is no Java in this package and no copy of the program. The launcher finds an installed ResearchZosho
|
|
15
|
+
and starts `researchzosho mcp`. When none is installed it fetches the release of the same version from GitHub,
|
|
16
|
+
checks it against the release's own `SHA256SUMS` (no sums, no install), unpacks it under `~/.researchzosho/launcher`,
|
|
17
|
+
and starts it: the small tarball when Java 21 or newer is on the machine, otherwise the build for this platform
|
|
18
|
+
that carries its own runtime, so nothing has to be installed first. A machine with no library gets one made at `~/researchzosho-library`; `researchzosho setup`
|
|
19
|
+
names a model server and installs the service, and the pages at http://127.0.0.1:4649/ come with it.
|
|
20
|
+
|
|
21
|
+
Nothing but the MCP stream is written to stdout. Every message from the launcher goes to stderr.
|
|
22
|
+
|
|
23
|
+
| variable | meaning |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `RESEARCHZOSHO_PREFIX` | where the one-line installer put the program (default `~/.local`, or `%LOCALAPPDATA%\Programs`) |
|
|
26
|
+
| `RESEARCHZOSHO_LIBRARY` | the library folder (default `~/researchzosho-library`) |
|
|
27
|
+
|
|
28
|
+
The version of this package is the version of ResearchZosho it starts. Docs:
|
|
29
|
+
[LIBRARIAN_HOWTOUSE.md](https://github.com/Wyrdsekai/researchzosho/blob/main/docs/LIBRARIAN_HOWTOUSE.md),
|
|
30
|
+
[LIBRARY_PROTOCOL.md](https://github.com/Wyrdsekai/researchzosho/blob/main/docs/LIBRARY_PROTOCOL.md).
|
|
31
|
+
License: Apache 2.0.
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/*
|
|
4
|
+
* @wyrdsekai/researchzosho-mcp — starts ResearchZosho's MCP server (`researchzosho mcp`) over stdio, so any MCP client
|
|
5
|
+
* can run it with `npx @wyrdsekai/researchzosho-mcp`.
|
|
6
|
+
*
|
|
7
|
+
* There is no Java in this package. It finds an installed ResearchZosho and starts it; failing that it fetches the
|
|
8
|
+
* release this version names from GitHub, checks it against the release's own SHA256SUMS (no sums, no install),
|
|
9
|
+
* unpacks it under ~/.researchzosho/launcher, and starts it. With Java 21 or newer on the machine that is the small
|
|
10
|
+
* tarball; without it, the build for this platform that carries its own runtime (linux, macOS and Windows, x64 and
|
|
11
|
+
* arm64 where the release has them). A release with no such build prints the install one-liner and exits 1.
|
|
12
|
+
*
|
|
13
|
+
* stdout is the MCP stream and nothing else is ever written to it. Every message here goes to stderr.
|
|
14
|
+
*
|
|
15
|
+
* RESEARCHZOSHO_PREFIX where the one-line installer put the program (default ~/.local, or %LOCALAPPDATA%\Programs)
|
|
16
|
+
* RESEARCHZOSHO_DOWNLOAD_BASE test hook: fetch the tarball and SHA256SUMS from here instead of GitHub
|
|
17
|
+
*/
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
const { spawn, spawnSync } = require('child_process');
|
|
23
|
+
const { pipeline } = require('stream/promises');
|
|
24
|
+
const { Readable } = require('stream');
|
|
25
|
+
|
|
26
|
+
const TOOL = 'researchzosho';
|
|
27
|
+
const TITLE = 'ResearchZosho';
|
|
28
|
+
const ENV = 'RESEARCHZOSHO';
|
|
29
|
+
const REPO = 'Wyrdsekai/researchzosho';
|
|
30
|
+
const SITE = 'https://researchzosho.org';
|
|
31
|
+
const VERSION = require('../package.json').version;
|
|
32
|
+
/** The release this launcher starts: the package version without an npm-only suffix (0.1.6-1 is the launcher, 0.1.6 the release). */
|
|
33
|
+
const RELEASE = VERSION.replace(/-.*$/, '');
|
|
34
|
+
const WIN = process.platform === 'win32';
|
|
35
|
+
const HOME = os.homedir();
|
|
36
|
+
|
|
37
|
+
function say(message) { process.stderr.write(`${TOOL}-mcp: ${message}\n`); }
|
|
38
|
+
|
|
39
|
+
/** The program's start script inside an unpacked release folder. */
|
|
40
|
+
function startScript(dir) { return path.join(dir, 'bin', WIN ? `${TOOL}.bat` : TOOL); }
|
|
41
|
+
function runnable(p) { try { return p && fs.statSync(p).isFile(); } catch (e) { return false; } }
|
|
42
|
+
|
|
43
|
+
/** An installed ResearchZosho: the installer's prefix, PATH, the known roots, then this launcher's own cache. */
|
|
44
|
+
function installed() {
|
|
45
|
+
const found = [];
|
|
46
|
+
const prefix = process.env[`${ENV}_PREFIX`];
|
|
47
|
+
if (prefix) {
|
|
48
|
+
if (WIN) found.push(startScript(path.join(prefix, TOOL)));
|
|
49
|
+
else found.push(path.join(prefix, 'bin', TOOL), startScript(path.join(prefix, 'share', TOOL)));
|
|
50
|
+
}
|
|
51
|
+
const names = WIN ? [`${TOOL}.bat`, `${TOOL}.cmd`, `${TOOL}.exe`] : [TOOL];
|
|
52
|
+
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
|
53
|
+
if (!dir) continue;
|
|
54
|
+
for (const n of names) found.push(path.join(dir, n));
|
|
55
|
+
}
|
|
56
|
+
if (WIN) {
|
|
57
|
+
if (process.env.LOCALAPPDATA) found.push(startScript(path.join(process.env.LOCALAPPDATA, 'Programs', TOOL)));
|
|
58
|
+
} else {
|
|
59
|
+
found.push(path.join(HOME, '.local', 'bin', TOOL), path.join('/usr/local/bin', TOOL), path.join('/usr/bin', TOOL), startScript(path.join('/opt', TOOL)));
|
|
60
|
+
}
|
|
61
|
+
found.push(startScript(path.join(cacheRoot(), `${TOOL}-${RELEASE}`, TOOL)));
|
|
62
|
+
return found.find(runnable) || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function cacheRoot() { return path.join(HOME, `.${TOOL}`, 'launcher'); }
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The version an installed program is, read off the jar beside its start script — or, through the installer's
|
|
69
|
+
* wrapper (`exec "<root>/bin/<tool>" "$@"`), the root it names; null when it cannot be told without starting it.
|
|
70
|
+
*/
|
|
71
|
+
function installedVersion(script) {
|
|
72
|
+
let root = path.resolve(path.dirname(script), '..');
|
|
73
|
+
if (!fs.existsSync(path.join(root, 'lib'))) {
|
|
74
|
+
try {
|
|
75
|
+
const m = /exec "([^"]+)\/bin\/[^"/]+"/.exec(fs.readFileSync(script, 'utf8'));
|
|
76
|
+
if (m) root = m[1];
|
|
77
|
+
} catch (e) { return null; }
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const re = new RegExp(`^(?:${TOOL}|librarian|core)-(\\d+\\.\\d+\\.\\d+)\\.jar$`);
|
|
81
|
+
for (const f of fs.readdirSync(path.join(root, 'lib'))) { const m = re.exec(f); if (m) return m[1]; }
|
|
82
|
+
} catch (e) { /* not a layout we know */ }
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The major version of the java on PATH, or 0. */
|
|
87
|
+
function javaMajor() {
|
|
88
|
+
const r = spawnSync('java', ['-version'], { encoding: 'utf8', windowsHide: true });
|
|
89
|
+
if (r.error || r.status !== 0) return 0;
|
|
90
|
+
const m = /version "(\d+)/.exec(`${r.stderr}${r.stdout}`);
|
|
91
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Fetch to a file, streamed: a 60 MB runtime build never sits in memory whole. */
|
|
95
|
+
async function fetchTo(url, file) {
|
|
96
|
+
const r = await fetch(url, { redirect: 'follow' });
|
|
97
|
+
if (!r.ok || !r.body) throw new Error(`HTTP ${r.status} for ${url}`);
|
|
98
|
+
await pipeline(Readable.fromWeb(r.body), fs.createWriteStream(file));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The sha256 SHA256SUMS lists for a bare file name, or null. */
|
|
102
|
+
function expectedSum(sums, name) {
|
|
103
|
+
for (const line of sums.split(/\r?\n/)) {
|
|
104
|
+
const m = /^([0-9a-f]{64})\s+\*?(?:\.\/)?(.+?)\s*$/.exec(line);
|
|
105
|
+
if (m && m[2] === name) return m[1];
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The release's name for a build with its own runtime on this machine, or null where there is none. */
|
|
111
|
+
function platformBuild() {
|
|
112
|
+
const os = { linux: 'linux', darwin: 'macos', win32: 'windows' }[process.platform];
|
|
113
|
+
const arch = { x64: 'x64', arm64: 'arm64' }[process.arch];
|
|
114
|
+
if (!os || !arch || (os === 'windows' && arch !== 'x64')) return null;
|
|
115
|
+
return `${os}-${arch}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Fetch, check and unpack the release this launcher names; returns the start script. */
|
|
119
|
+
async function download() {
|
|
120
|
+
const major = javaMajor();
|
|
121
|
+
const noJava = major ? `java ${major} found; ${TITLE} needs Java 21 or newer.` : `java not found; ${TITLE} needs Java 21 or newer on PATH (https://adoptium.net).`;
|
|
122
|
+
const base = process.env[`${ENV}_DOWNLOAD_BASE`] || `https://github.com/${REPO}/releases/download/v${RELEASE}`;
|
|
123
|
+
let tar = `${TOOL}-${RELEASE}.tar.gz`;
|
|
124
|
+
if (major < 21) {
|
|
125
|
+
const build = platformBuild();
|
|
126
|
+
if (!build) { say(noJava); say(`Install ${TITLE} itself and this launcher will start it: ${SITE}`); process.exit(1); }
|
|
127
|
+
tar = `${TOOL}-${RELEASE}-${build}.tar.gz`;
|
|
128
|
+
say(`${noJava} Fetching the ${build} build that carries its own runtime.`);
|
|
129
|
+
}
|
|
130
|
+
const root = cacheRoot();
|
|
131
|
+
const dest = path.join(root, `${TOOL}-${RELEASE}`);
|
|
132
|
+
const part = path.join(root, `.part-${process.pid}`);
|
|
133
|
+
fs.mkdirSync(part, { recursive: true });
|
|
134
|
+
try {
|
|
135
|
+
say(`fetching ${tar} from ${base}`);
|
|
136
|
+
try { await fetchTo(`${base}/${tar}`, path.join(part, tar)); }
|
|
137
|
+
catch (e) {
|
|
138
|
+
if (major < 21 && /HTTP 404/.test(e.message)) throw new Error(`this release has no build with its own runtime for this machine (${e.message}); install Java 21 or newer (https://adoptium.net)`);
|
|
139
|
+
throw e;
|
|
140
|
+
}
|
|
141
|
+
let sums;
|
|
142
|
+
try { await fetchTo(`${base}/SHA256SUMS`, path.join(part, 'SHA256SUMS')); sums = fs.readFileSync(path.join(part, 'SHA256SUMS'), 'utf8'); }
|
|
143
|
+
catch (e) { throw new Error(`no SHA256SUMS in the release (${e.message}). Refusing to install something unchecked`); }
|
|
144
|
+
const want = expectedSum(sums, tar);
|
|
145
|
+
if (!want) throw new Error(`${tar} is not listed in SHA256SUMS`);
|
|
146
|
+
const got = crypto.createHash('sha256').update(fs.readFileSync(path.join(part, tar))).digest('hex');
|
|
147
|
+
if (got !== want) throw new Error(`checksum mismatch for ${tar}. Refusing to install\n expected ${want}\n got ${got}`);
|
|
148
|
+
say('checksum verified');
|
|
149
|
+
const x = spawnSync('tar', ['xzf', path.join(part, tar), '-C', part], { encoding: 'utf8', windowsHide: true });
|
|
150
|
+
if (x.error || x.status !== 0) throw new Error(`could not unpack ${tar}: ${x.error ? x.error.message : x.stderr}`);
|
|
151
|
+
fs.rmSync(path.join(part, tar), { force: true });
|
|
152
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
153
|
+
fs.renameSync(part, dest);
|
|
154
|
+
if (!WIN) fs.chmodSync(startScript(path.join(dest, TOOL)), 0o755);
|
|
155
|
+
} catch (e) {
|
|
156
|
+
fs.rmSync(part, { recursive: true, force: true });
|
|
157
|
+
say(e.message);
|
|
158
|
+
say(`Or install ${TITLE} itself: ${SITE}`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
say(`installed ${dest}`);
|
|
162
|
+
return startScript(path.join(dest, TOOL));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function run(script, args) {
|
|
166
|
+
const child = WIN
|
|
167
|
+
? spawn('cmd.exe', ['/d', '/s', '/c', `""${script}" ${args.join(' ')}"`], { stdio: 'inherit', windowsHide: true, windowsVerbatimArguments: true }) // /s strips the outer pair of quotes
|
|
168
|
+
: spawn(script, args, { stdio: 'inherit' });
|
|
169
|
+
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
170
|
+
try { process.on(sig, () => { try { child.kill(sig); } catch (e) { /* already gone */ } }); } catch (e) { /* not on this platform */ }
|
|
171
|
+
}
|
|
172
|
+
child.on('error', (e) => { say(`could not start ${script}: ${e.message}`); process.exit(1); });
|
|
173
|
+
child.on('exit', (code, signal) => process.exit(code == null ? (signal ? 1 : 0) : code));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
(async () => {
|
|
177
|
+
const args = process.argv.slice(2);
|
|
178
|
+
if (args[0] === '--help' || args[0] === '-h') {
|
|
179
|
+
process.stderr.write(`usage: npx @wyrdsekai/${TOOL}-mcp\nStarts \`${TOOL} mcp\` (the MCP server over stdio), installing ${TITLE} ${RELEASE} first when it is not on this machine.\n`);
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
if (args[0] === '--version') { process.stderr.write(`${TOOL}-mcp ${VERSION}\n`); process.exit(0); }
|
|
183
|
+
let script = installed();
|
|
184
|
+
if (script) {
|
|
185
|
+
const v = installedVersion(script);
|
|
186
|
+
say(`starting ${script} mcp${v ? ` (${TITLE} ${v})` : ''}`);
|
|
187
|
+
// the registry lists this launcher as VERSION; an older install on the machine is what actually answers
|
|
188
|
+
if (v && v !== RELEASE) say(`note: the installed ${TITLE} is ${v}, this launcher starts ${RELEASE}; \`${TOOL} update now\` brings the install up to date`);
|
|
189
|
+
} else script = await download();
|
|
190
|
+
run(script, ['mcp', ...args]);
|
|
191
|
+
})().catch((e) => { say(e.message); process.exit(1); });
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wyrdsekai/researchzosho-mcp",
|
|
3
|
+
"version": "0.1.6-1",
|
|
4
|
+
"description": "Starts ResearchZosho, the research librarian for the rest of us, as an MCP server over stdio: npx @wyrdsekai/researchzosho-mcp",
|
|
5
|
+
"mcpName": "io.github.Wyrdsekai/researchzosho",
|
|
6
|
+
"bin": {
|
|
7
|
+
"researchzosho-mcp": "bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test test/launcher.test.js"
|
|
18
|
+
},
|
|
19
|
+
"license": "Apache-2.0",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Wyrdsekai/researchzosho.git",
|
|
23
|
+
"directory": "npm"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://researchzosho.org",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/Wyrdsekai/researchzosho/issues"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"mcp",
|
|
31
|
+
"modelcontextprotocol",
|
|
32
|
+
"research",
|
|
33
|
+
"library",
|
|
34
|
+
"citations",
|
|
35
|
+
"local-model"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
}
|
|
40
|
+
}
|