tinyfish-mcp-lite 0.1.0 → 0.2.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 +45 -17
- package/dist/config.js +1 -3
- package/dist/http/adapter.js +1 -1
- package/dist/index.js +44 -2
- package/dist/stdio/adapter.d.ts +13 -0
- package/dist/stdio/adapter.js +267 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tinyfish-mcp-lite
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/tinyfish-mcp-lite)
|
|
4
4
|
|
|
5
5
|
> **⚠️ You are on `tinyfish-mcp-lite`** — a fork that exposes **only the two
|
|
6
6
|
> free hosted tools** (`search`, `fetch_content`) and hides the 26 paid ones,
|
|
@@ -51,31 +51,59 @@ this fork too — with exactly these differences:
|
|
|
51
51
|
|
|
52
52
|
### Install (this fork)
|
|
53
53
|
|
|
54
|
-
The
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
The fork ships on npm as **`tinyfish-mcp-lite`** (requires Node.js >= 22).
|
|
55
|
+
Two transports are available:
|
|
56
|
+
|
|
57
|
+
**stdio — one command, the client manages the process.** Most clients that
|
|
58
|
+
launch MCP servers as commands (Claude Code with a `command` entry, Cursor,
|
|
59
|
+
Zed, many launcher UIs) want this:
|
|
57
60
|
|
|
58
61
|
```sh
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
# the client spawns it; add TINYFISH_API_KEY to the command's environment
|
|
63
|
+
npx -y tinyfish-mcp-lite --stdio
|
|
64
|
+
|
|
65
|
+
# or with the global install (`npm i -g tinyfish-mcp-lite`):
|
|
66
|
+
tinyfish-mcp --stdio
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
In a client config that takes a command + env vars, that is:
|
|
62
70
|
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"command": "npx",
|
|
74
|
+
"args": ["-y", "tinyfish-mcp-lite", "--stdio"],
|
|
75
|
+
"env": { "TINYFISH_API_KEY": "tf_..." }
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**HTTP — one shared server for several clients.** Start it once (terminal,
|
|
80
|
+
systemd, pm2) and point every client at the same URL:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
TINYFISH_API_KEY=tf_... npx tinyfish-mcp-lite # no --stdio flag
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```sh
|
|
63
87
|
# or from a clone
|
|
64
88
|
git clone https://github.com/ByronFinn/tinyfish-mcp-lite.git
|
|
65
89
|
cd tinyfish-mcp-lite
|
|
66
90
|
npm ci && npm run build
|
|
67
|
-
TINYFISH_API_KEY=tf_... node dist/index.js
|
|
91
|
+
TINYFISH_API_KEY=tf_... node dist/index.js # HTTP
|
|
92
|
+
# TINYFISH_API_KEY=tf_... node dist/index.js --stdio # stdio
|
|
68
93
|
```
|
|
69
94
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
>
|
|
74
|
-
>
|
|
75
|
-
>
|
|
95
|
+
Both transports expose the same filtered proxy; stdio just has no port and
|
|
96
|
+
no manual startup — closing the client's pipe shuts the process down.
|
|
97
|
+
|
|
98
|
+
> **Note on git-based installs** (`npm i -g github:ByronFinn/tinyfish-mcp-lite`):
|
|
99
|
+
> npm ≥ 11.17 with the install-script policy enabled (an `allow-scripts`
|
|
100
|
+
> entry in your `~/.npmrc`) rejects them with `EALLOWSCRIPTS` — the policy
|
|
101
|
+
> leaks into the inner git-dep build via the environment. Use the npm or
|
|
102
|
+
> clone routes instead; registry installs (`npx tinyfish-mcp-lite`) are
|
|
103
|
+
> unaffected.
|
|
76
104
|
|
|
77
|
-
|
|
78
|
-
described in [Client configuration](#client-configuration) below.
|
|
105
|
+
For the HTTP transport, point your MCP client at `http://127.0.0.1:3711/mcp`
|
|
106
|
+
as described in [Client configuration](#client-configuration) below.
|
|
79
107
|
|
|
80
108
|
Why a fork at all: the hosted server's paid tools (automation, browsers,
|
|
81
109
|
monitors…) are unusable without wallet funds, but their schemas still land
|
package/dist/config.js
CHANGED
|
@@ -6,9 +6,7 @@ export const API_KEY_GUIDANCE = "Set TINYFISH_API_KEY — get a key at https://a
|
|
|
6
6
|
export class ConfigError extends Error {
|
|
7
7
|
}
|
|
8
8
|
const envSchema = z.object({
|
|
9
|
-
TINYFISH_API_KEY: z
|
|
10
|
-
.string({ error: API_KEY_GUIDANCE })
|
|
11
|
-
.min(1, { error: API_KEY_GUIDANCE }),
|
|
9
|
+
TINYFISH_API_KEY: z.string({ error: API_KEY_GUIDANCE }).min(1, { error: API_KEY_GUIDANCE }),
|
|
12
10
|
PORT: z
|
|
13
11
|
.string()
|
|
14
12
|
.optional()
|
package/dist/http/adapter.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { randomUUID } from "node:crypto";
|
|
26
26
|
import { JsonRpcErrorCodes, LocalWriteError, ProxyCoreError, toJsonRpcError, toStreamErrorFrame, } from "../core/errors.js";
|
|
27
|
-
import { requestIdOf } from "../core/proxy-core.js";
|
|
27
|
+
import { requestIdOf, } from "../core/proxy-core.js";
|
|
28
28
|
import { log } from "../log.js";
|
|
29
29
|
export function createMcpAdapter(core) {
|
|
30
30
|
return async (req, res) => {
|
package/dist/index.js
CHANGED
|
@@ -11,14 +11,30 @@ import { VERSION } from "./version.js";
|
|
|
11
11
|
function isErrnoException(err) {
|
|
12
12
|
return err instanceof Error && "code" in err;
|
|
13
13
|
}
|
|
14
|
+
// FORK(lite): stdio shutdown hook runner — mirrors the HTTP branch's shutdown
|
|
15
|
+
// path (core.closeAll aborts in-flight upstream fetches).
|
|
16
|
+
async function runShutdownHooks() {
|
|
17
|
+
for (const hook of shutdownHooks) {
|
|
18
|
+
try {
|
|
19
|
+
await hook();
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
log.error(`shutdown hook failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
14
26
|
async function main() {
|
|
27
|
+
// FORK(lite): stdio owns no port — drop PORT from the environment the
|
|
28
|
+
// config parses, so a stray PORT (e.g. copied from another server's client
|
|
29
|
+
// config block) cannot abort a transport that never listens.
|
|
30
|
+
const env = process.argv.includes("--stdio") ? { ...process.env, PORT: undefined } : process.env;
|
|
15
31
|
let config;
|
|
16
32
|
let toolPolicy; // FORK(lite)
|
|
17
33
|
try {
|
|
18
|
-
config = parseConfig(
|
|
34
|
+
config = parseConfig(env);
|
|
19
35
|
// FORK(lite): TINYFISH_TOOLS allowlist (default: the free search +
|
|
20
36
|
// fetch_content; "*" restores unfiltered upstream passthrough).
|
|
21
|
-
toolPolicy = parseToolPolicy(
|
|
37
|
+
toolPolicy = parseToolPolicy(env.TINYFISH_TOOLS);
|
|
22
38
|
}
|
|
23
39
|
catch (err) {
|
|
24
40
|
if (err instanceof ConfigError) {
|
|
@@ -33,6 +49,32 @@ async function main() {
|
|
|
33
49
|
// answers tools/call for them locally — paid tools are never advertised
|
|
34
50
|
// and never invoked upstream.
|
|
35
51
|
const core = withToolFilter(createProxyCore({ upstreamUrl: config.upstreamUrl, apiKey: config.apiKey }), toolPolicy);
|
|
52
|
+
// FORK(lite): stdio transport — spawn-and-forget mode for clients that
|
|
53
|
+
// launch their MCP servers as commands (`tinyfish-mcp --stdio`, or
|
|
54
|
+
// `npx -y tinyfish-mcp-lite --stdio`). Same core, same filter; the client
|
|
55
|
+
// owns the lifecycle (stdin closed ⇒ exit), so no PORT and no long-running
|
|
56
|
+
// server. See src/stdio/adapter.ts and FORK.md.
|
|
57
|
+
if (process.argv.includes("--stdio")) {
|
|
58
|
+
const { runStdioTransport } = await import("./stdio/adapter.js");
|
|
59
|
+
log.info(`stdio transport — upstream ${config.upstreamUrl} — v${VERSION}`);
|
|
60
|
+
if (toolPolicy.mode === "allow") {
|
|
61
|
+
log.info(`tool filter: exposing [${[...toolPolicy.allowed].join(", ")}] — set TINYFISH_TOOLS=* to expose every upstream tool`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
log.info("tool filter: off (TINYFISH_TOOLS=*) — exposing every upstream tool");
|
|
65
|
+
}
|
|
66
|
+
// The client normally shuts us down by closing stdin; a signal (client
|
|
67
|
+
// killed, terminal gone) must still run the hooks — closeAll aborts
|
|
68
|
+
// in-flight upstream fetches — instead of the default hard exit.
|
|
69
|
+
const onSignal = () => {
|
|
70
|
+
void runShutdownHooks().then(() => process.exit(0));
|
|
71
|
+
};
|
|
72
|
+
process.on("SIGINT", onSignal);
|
|
73
|
+
process.on("SIGTERM", onSignal);
|
|
74
|
+
await runStdioTransport(core);
|
|
75
|
+
await runShutdownHooks();
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
36
78
|
const handler = createAppHandler(createMcpAdapter(core));
|
|
37
79
|
let server;
|
|
38
80
|
try {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
import { type ProxyCore } from "../core/proxy-core.js";
|
|
3
|
+
export interface StdioStreams {
|
|
4
|
+
input: Readable;
|
|
5
|
+
output: Writable;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Run the transport until `streams.input` ends or errors (the client closed
|
|
9
|
+
* or lost its pipe — MCP stdio shutdown) or `streams.output` errors/closes
|
|
10
|
+
* (EPIPE: the client died without closing stdin). Resolves void; the caller
|
|
11
|
+
* runs shutdown hooks and exits.
|
|
12
|
+
*/
|
|
13
|
+
export declare function runStdioTransport(core: ProxyCore, streams?: StdioStreams): Promise<void>;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FORK ADDITION (tinyfish-mcp-lite) — this module does not exist upstream.
|
|
3
|
+
*
|
|
4
|
+
* stdio transport: spawn-and-forget mode for MCP clients that launch their
|
|
5
|
+
* servers as commands. The client owns the process lifecycle (stdin closed ⇒
|
|
6
|
+
* shutdown), no port to manage, no server to start by hand. Newline-delimited
|
|
7
|
+
* JSON-RPC on stdin/stdout per the MCP stdio spec; every log goes to stderr
|
|
8
|
+
* (log.ts) so stdout stays protocol-pure — log.ts documents exactly this
|
|
9
|
+
* contract ("stdout stays clean for a future stdio transport").
|
|
10
|
+
*
|
|
11
|
+
* Routing mirrors the HTTP adapter's route() one-to-one (notifications →
|
|
12
|
+
* notify BEFORE any tools/call check; initialize → initialize; tools/call →
|
|
13
|
+
* forwardStream; everything else, including forbidden batch arrays and
|
|
14
|
+
* shapeless bodies, → forward), so the withToolFilter decorator covers both
|
|
15
|
+
* transports identically: tools/list filtered, hidden tools/call answered
|
|
16
|
+
* locally, notification-shaped calls dropped, batches rejected — see
|
|
17
|
+
* tests/stdio.test.ts and tests/tool-filter.test.ts.
|
|
18
|
+
*
|
|
19
|
+
* Differences from the HTTP hop that are intrinsic to stdio:
|
|
20
|
+
* - No status codes, no headers: upstream's JSON-RPC body is the message;
|
|
21
|
+
* a JSON-RPC error body relays verbatim, and failures upstream never
|
|
22
|
+
* answered as JSON-RPC are shaped through toJsonRpcError (same as HTTP).
|
|
23
|
+
* A notification whose upstream hop fails writes NOTHING — JSON-RPC forbids
|
|
24
|
+
* replying to notifications, and an id-null error line would be protocol
|
|
25
|
+
* garbage to an id-dispatching client; the failure is logged to stderr.
|
|
26
|
+
* - One client, one session: the localKey is the empty string — the same
|
|
27
|
+
* session-less shape the HTTP hop uses — so before initialize bridges the
|
|
28
|
+
* real upstream session id, upstream.post omits Mcp-Session-Id entirely and
|
|
29
|
+
* no adapter-invented id can reach the wire. While an initialize round-trip
|
|
30
|
+
* is outstanding, further lines are queued (JSON-RPC pipelining is legal,
|
|
31
|
+
* and the MCP lifecycle even allows pings in this window) and dispatched in
|
|
32
|
+
* arrival order once it settles, so they ride the bridged upstream session.
|
|
33
|
+
* - A parse error answers with id null (JSON-RPC rule for unparseable input)
|
|
34
|
+
* rather than the HTTP hop's -1: there is no upstream shape to stay
|
|
35
|
+
* byte-compatible with on this transport.
|
|
36
|
+
* - Progress notifications from a streamed tools/call relay to stdout as
|
|
37
|
+
* ordinary JSON-RPC notification lines (MCP stdio allows server → client
|
|
38
|
+
* notifications any time). onEvent returns the write promise so the core's
|
|
39
|
+
* frame loop awaits each relayed frame — stdout backpressure propagates to
|
|
40
|
+
* the upstream stream (the OnEvent contract) and a dead stdout surfaces as
|
|
41
|
+
* LocalWriteError instead of being silently swallowed.
|
|
42
|
+
*
|
|
43
|
+
* Writes are queued (requests dispatch concurrently — a slow search must not
|
|
44
|
+
* stall pings — so responses may become ready out of order, which JSON-RPC
|
|
45
|
+
* ids disambiguate; the queue serializes the write() calls, so lines can
|
|
46
|
+
* never interleave mid-line) and each write respects pipe backpressure.
|
|
47
|
+
* Shutdown (stdin close, stdin error, stdout error or close) aborts in-flight
|
|
48
|
+
* upstream fetches via core.close, waits for the outstanding handlers so
|
|
49
|
+
* their final (often error) responses still reach a client that is draining
|
|
50
|
+
* stdout, then drains the write queue before resolving — the caller's exit
|
|
51
|
+
* never truncates a half-written line.
|
|
52
|
+
*/
|
|
53
|
+
import { createInterface } from "node:readline";
|
|
54
|
+
import { ProxyCoreError, toJsonRpcError } from "../core/errors.js";
|
|
55
|
+
import { requestIdOf } from "../core/proxy-core.js";
|
|
56
|
+
import { log } from "../log.js";
|
|
57
|
+
/** stdio serves exactly one client; "" matches the HTTP hop's session-less shape. */
|
|
58
|
+
const SESSION_KEY = "";
|
|
59
|
+
/**
|
|
60
|
+
* Run the transport until `streams.input` ends or errors (the client closed
|
|
61
|
+
* or lost its pipe — MCP stdio shutdown) or `streams.output` errors/closes
|
|
62
|
+
* (EPIPE: the client died without closing stdin). Resolves void; the caller
|
|
63
|
+
* runs shutdown hooks and exits.
|
|
64
|
+
*/
|
|
65
|
+
export function runStdioTransport(core, streams = { input: process.stdin, output: process.stdout }) {
|
|
66
|
+
let resolveRun;
|
|
67
|
+
const run = new Promise((resolve) => {
|
|
68
|
+
resolveRun = resolve;
|
|
69
|
+
});
|
|
70
|
+
let writeChain = Promise.resolve();
|
|
71
|
+
let finished = false;
|
|
72
|
+
let inflight = 0;
|
|
73
|
+
let inflightSettled = null;
|
|
74
|
+
let initGate = null;
|
|
75
|
+
const gatedLines = [];
|
|
76
|
+
/**
|
|
77
|
+
* Chain a write onto the queue and hand the promise back — onEvent returns
|
|
78
|
+
* it so upstream SSE frames are relayed one awaited write at a time.
|
|
79
|
+
*/
|
|
80
|
+
const enqueue = (message) => {
|
|
81
|
+
const write = writeChain.then(() => writeLine(streams.output, message));
|
|
82
|
+
// Keep the queue alive after a failed write (dying client); `write`
|
|
83
|
+
// itself still rejects, preserving the LocalWriteError contract.
|
|
84
|
+
writeChain = write.then(() => undefined, () => undefined);
|
|
85
|
+
return write;
|
|
86
|
+
};
|
|
87
|
+
/** Fire-and-forget enqueue for responses; a failed stdout write is logged. */
|
|
88
|
+
const emit = (message) => {
|
|
89
|
+
void enqueue(message).catch((err) => {
|
|
90
|
+
log.warn(`writing to stdout failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
const finish = () => {
|
|
94
|
+
if (finished)
|
|
95
|
+
return;
|
|
96
|
+
finished = true;
|
|
97
|
+
input.close();
|
|
98
|
+
void (async () => {
|
|
99
|
+
// Abort in-flight upstream fetches for this session so the handlers
|
|
100
|
+
// below settle promptly (idempotent — the caller's shutdown hooks run
|
|
101
|
+
// core.closeAll() on top, covering the rest of the session store).
|
|
102
|
+
core.close(SESSION_KEY);
|
|
103
|
+
// Let the outstanding handlers land their final responses before the
|
|
104
|
+
// drain below captures the queue, so a client still draining stdout
|
|
105
|
+
// receives them instead of a truncated stream.
|
|
106
|
+
if (inflight > 0) {
|
|
107
|
+
await new Promise((resolve) => {
|
|
108
|
+
inflightSettled = resolve;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
await writeChain;
|
|
112
|
+
resolveRun();
|
|
113
|
+
})();
|
|
114
|
+
};
|
|
115
|
+
const settleInflight = () => {
|
|
116
|
+
inflight -= 1;
|
|
117
|
+
if (inflight === 0 && inflightSettled !== null) {
|
|
118
|
+
const settle = inflightSettled;
|
|
119
|
+
inflightSettled = null;
|
|
120
|
+
settle();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
// Relayed SSE progress frames (free tools never stream today, but the
|
|
124
|
+
// relay must exist so the stdio path can never orphan upstream progress).
|
|
125
|
+
const onEvent = (event) => enqueue(event);
|
|
126
|
+
async function routeMessage(message) {
|
|
127
|
+
try {
|
|
128
|
+
if (isNotification(message)) {
|
|
129
|
+
await core.notify(SESSION_KEY, message);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const method = methodOf(message);
|
|
133
|
+
if (method === "initialize") {
|
|
134
|
+
const response = await core.initialize(SESSION_KEY, message, protocolVersionOf(message));
|
|
135
|
+
emit(response.body);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (method === "tools/call") {
|
|
139
|
+
const response = await core.forwardStream(SESSION_KEY, message, onEvent);
|
|
140
|
+
emit(response.body);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// Everything else — ping, tools/list, resources/*, client responses to
|
|
144
|
+
// (never-initiated) server requests, unknown methods, and batch arrays —
|
|
145
|
+
// forwards generically, byte-for-byte the HTTP adapter's fallthrough.
|
|
146
|
+
const response = await core.forward(SESSION_KEY, message);
|
|
147
|
+
emit(response.body);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
// Same classification as the HTTP adapter: classified core errors log
|
|
151
|
+
// their message; anything else is a proxy bug (full stack, generic
|
|
152
|
+
// InternalError to the client).
|
|
153
|
+
if (err instanceof ProxyCoreError) {
|
|
154
|
+
log.warn(err.message);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
log.error(`proxy bug (client got a generic InternalError): ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
158
|
+
}
|
|
159
|
+
// A notification has no response channel — JSON-RPC forbids replying
|
|
160
|
+
// to one, so its upstream failure only logs (see module comment).
|
|
161
|
+
if (!isNotification(message)) {
|
|
162
|
+
emit(toJsonRpcError(err, requestIdOf(message)).body);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function dispatchMessage(message) {
|
|
167
|
+
inflight += 1;
|
|
168
|
+
try {
|
|
169
|
+
await routeMessage(message);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
settleInflight();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function handleLine(line) {
|
|
176
|
+
if (finished)
|
|
177
|
+
return;
|
|
178
|
+
const trimmed = line.trim();
|
|
179
|
+
if (trimmed === "")
|
|
180
|
+
return;
|
|
181
|
+
if (initGate !== null) {
|
|
182
|
+
// Pipelined behind an in-flight initialize: queued, dispatched in
|
|
183
|
+
// arrival order once it settles (see module comment).
|
|
184
|
+
gatedLines.push(trimmed);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
let message;
|
|
188
|
+
try {
|
|
189
|
+
message = JSON.parse(trimmed);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Unparseable input: the one message answered without forwarding.
|
|
193
|
+
emit({
|
|
194
|
+
jsonrpc: "2.0",
|
|
195
|
+
error: { code: -32700, message: "Parse error: Invalid JSON" },
|
|
196
|
+
id: null,
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (!isNotification(message) && methodOf(message) === "initialize") {
|
|
201
|
+
// Install the gate before any await so lines arriving during the
|
|
202
|
+
// round-trip hit the queue above, then flush it in order.
|
|
203
|
+
const gate = dispatchMessage(message);
|
|
204
|
+
initGate = gate;
|
|
205
|
+
void gate.finally(() => {
|
|
206
|
+
initGate = null;
|
|
207
|
+
if (!finished) {
|
|
208
|
+
for (const queued of gatedLines.splice(0))
|
|
209
|
+
handleLine(queued);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
void dispatchMessage(message);
|
|
215
|
+
}
|
|
216
|
+
const input = createInterface({ input: streams.input });
|
|
217
|
+
input.on("line", (line) => {
|
|
218
|
+
handleLine(line);
|
|
219
|
+
});
|
|
220
|
+
// readline re-emits an input-stream error here; without this listener the
|
|
221
|
+
// 'error' event is unhandled (crash, hooks never run) and 'close' never
|
|
222
|
+
// fires afterwards — hence the explicit finish().
|
|
223
|
+
input.on("error", (err) => {
|
|
224
|
+
log.warn(`stdin error: ${err instanceof Error ? err.message : String(err)}`);
|
|
225
|
+
finish();
|
|
226
|
+
});
|
|
227
|
+
input.on("close", finish);
|
|
228
|
+
streams.output.on("error", (err) => {
|
|
229
|
+
log.warn(`stdout error (client went away?): ${err instanceof Error ? err.message : String(err)}`);
|
|
230
|
+
finish();
|
|
231
|
+
});
|
|
232
|
+
// stdout destroyed without an error event (silent destroy): still shut down.
|
|
233
|
+
streams.output.on("close", finish);
|
|
234
|
+
return run;
|
|
235
|
+
}
|
|
236
|
+
/** One JSON-RPC message per line; resolves on kernel-buffer flush or drain. */
|
|
237
|
+
function writeLine(output, message) {
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
const ok = output.write(`${JSON.stringify(message)}\n`, (err) => err ? reject(err) : resolve());
|
|
240
|
+
if (!ok)
|
|
241
|
+
output.once("drain", () => resolve());
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/** JSON-RPC notification: an object with a method and no id key (upstream's rule). */
|
|
245
|
+
function isNotification(message) {
|
|
246
|
+
return (typeof message === "object" &&
|
|
247
|
+
message !== null &&
|
|
248
|
+
!Array.isArray(message) &&
|
|
249
|
+
typeof message.method === "string" &&
|
|
250
|
+
!("id" in message));
|
|
251
|
+
}
|
|
252
|
+
function methodOf(message) {
|
|
253
|
+
if (typeof message !== "object" || message === null)
|
|
254
|
+
return undefined;
|
|
255
|
+
const method = message.method;
|
|
256
|
+
return typeof method === "string" ? method : undefined;
|
|
257
|
+
}
|
|
258
|
+
/** `initialize.params.protocolVersion`, when the client sent one. */
|
|
259
|
+
function protocolVersionOf(message) {
|
|
260
|
+
if (typeof message !== "object" || message === null)
|
|
261
|
+
return undefined;
|
|
262
|
+
const params = message.params;
|
|
263
|
+
if (typeof params !== "object" || params === null)
|
|
264
|
+
return undefined;
|
|
265
|
+
const version = params.protocolVersion;
|
|
266
|
+
return typeof version === "string" ? version : undefined;
|
|
267
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tinyfish-mcp-lite",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Fork of @tiny-fish/mcp exposing only the free TinyFish tools (search, fetch_content)
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Fork of @tiny-fish/mcp exposing only the free TinyFish tools (search, fetch_content) — local reverse proxy to agent.tinyfish.ai/mcp",
|
|
5
5
|
"mcpName": "io.github.ByronFinn/tinyfish-mcp-lite",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|