@reddb-io/mcp 1.16.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/LICENSE ADDED
@@ -0,0 +1,111 @@
1
+ Business Source License 1.1
2
+
3
+ Parameters
4
+
5
+ Licensor: RedDB.io <hello@reddb.io>
6
+
7
+ Licensed Work: RedDB
8
+ The Licensed Work is (c) 2026 RedDB.io
9
+
10
+ Additional Use Grant: You may make use of the Licensed Work, provided that
11
+ you do not use the Licensed Work for a Database
12
+ Service.
13
+
14
+ A "Database Service" is a commercial offering that
15
+ allows third parties (other than your employees and
16
+ individual contractors) to access the functionality
17
+ of the Licensed Work by creating tables, documents,
18
+ graphs, vectors, key-value entries, queues, or
19
+ time-series whose schemas and/or data are determined
20
+ by those third parties, or by querying, storing, or
21
+ retrieving data determined by those third parties,
22
+ whether as a hosted, managed, "as-a-service",
23
+ database-as-a-service (DBaaS), platform-as-a-service
24
+ (PaaS), software-as-a-service (SaaS), or cloud
25
+ database product.
26
+
27
+ For clarity, the Additional Use Grant permits, among
28
+ other things, the following uses, none of which
29
+ constitute a Database Service: personal use; internal
30
+ use within your own organization; research and
31
+ educational use; development and testing; use in
32
+ open source projects; and self-hosting the Licensed
33
+ Work for your own organization's own data.
34
+
35
+ Change Date: 2030-06-22
36
+
37
+ Change License: GNU Affero General Public License version 3.0
38
+
39
+ For information about alternative licensing arrangements for the Licensed
40
+ Work, please contact hello@reddb.io.
41
+
42
+ Notice
43
+
44
+ Business Source License 1.1
45
+
46
+ Terms
47
+
48
+ The Licensor hereby grants you the right to copy, modify, create derivative
49
+ works, redistribute, and make non-production use of the Licensed Work. The
50
+ Licensor may make an Additional Use Grant, above, permitting limited
51
+ production use.
52
+
53
+ Effective on the Change Date, or the fourth anniversary of the first publicly
54
+ available distribution of a specific version of the Licensed Work under this
55
+ License, whichever comes first, the Licensor hereby grants you rights under
56
+ the terms of the Change License, and the rights granted in the paragraph
57
+ above terminate.
58
+
59
+ If your use of the Licensed Work does not comply with the requirements
60
+ currently in effect as described in this License, you must purchase a
61
+ commercial license from the Licensor, its affiliated entities, or authorized
62
+ resellers, or you must refrain from using the Licensed Work.
63
+
64
+ All copies of the original and modified Licensed Work, and derivative works
65
+ of the Licensed Work, are subject to this License. This License applies
66
+ separately for each version of the Licensed Work and the Change Date may vary
67
+ for each version of the Licensed Work released by Licensor.
68
+
69
+ You must conspicuously display this License on each original or modified copy
70
+ of the Licensed Work. If you receive the Licensed Work in original or
71
+ modified form from a third party, the terms and conditions set forth in this
72
+ License apply to your use of that work.
73
+
74
+ Any use of the Licensed Work in violation of this License will automatically
75
+ terminate your rights under this License for the current and all other
76
+ versions of the Licensed Work.
77
+
78
+ This License does not grant you any right in any trademark or logo of
79
+ Licensor or its affiliates (provided that you may use a trademark or logo of
80
+ Licensor as expressly required by this License).
81
+
82
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
83
+ AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
84
+ EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
85
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
86
+ TITLE.
87
+
88
+ MariaDB hereby grants you permission to use this License's text to license
89
+ your works, and to refer to it using the trademark "Business Source License",
90
+ as long as you comply with the Covenants of Licensor below.
91
+
92
+ Covenants of Licensor
93
+
94
+ In consideration of the right to use this License's text and the "Business
95
+ Source License" name and trademark, Licensor covenants to MariaDB, and to all
96
+ other recipients of the licensed work to be provided by Licensor:
97
+
98
+ 1. To specify as the Change License the GPL Version 2.0 or any later version,
99
+ or a license that is compatible with GPL Version 2.0 or a later version,
100
+ where "compatible" means that software provided under the Change License can
101
+ be included in a program with software provided under GPL Version 2.0 or a
102
+ later version. Licensor may specify additional Change Licenses without
103
+ limitation.
104
+
105
+ 2. To either: (a) specify an additional grant of rights to use that does not
106
+ impose any additional restriction on the right granted in this License, as
107
+ the Additional Use Grant; or (b) insert the text "None".
108
+
109
+ 3. To specify a Change Date.
110
+
111
+ 4. Not to modify this License in any other way.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @reddb-io/mcp
2
+
3
+ Zero-install [MCP](https://modelcontextprotocol.io) launcher for [RedDB](https://github.com/reddb-io/reddb).
4
+
5
+ ```bash
6
+ npx -y @reddb-io/mcp
7
+ ```
8
+
9
+ That's the whole thing. The launcher resolves (or downloads) the matching
10
+ `red` engine binary from GitHub Releases and spawns `red mcp` over stdio.
11
+ Because `npx -y @reddb-io/mcp` always pulls the latest published launcher,
12
+ you always run the latest released engine — no local install, no stale
13
+ binary.
14
+
15
+ No tool or knowledge logic is reimplemented in JavaScript. The launcher
16
+ only fetches the native binary and execs `red mcp`; the MCP tool surface
17
+ and `red://` knowledge resources come from the engine itself.
18
+
19
+ ## Use it as an MCP server
20
+
21
+ Point your agent host at the launcher as a stdio MCP server:
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "reddb": {
27
+ "command": "npx",
28
+ "args": ["-y", "@reddb-io/mcp"],
29
+ "env": {
30
+ "REDDB_MCP_URI": "${REDDB_MCP_URI:-memory://}"
31
+ }
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ The default `memory://` URI starts an embedded ephemeral engine. To use a
38
+ file-backed or remote database, export `REDDB_MCP_URI` in the host environment
39
+ before your MCP host starts. Keep credentials in the host environment, not in
40
+ the committed `.mcp.json`.
41
+
42
+ Extra arguments are forwarded to `red mcp` after the subcommand, e.g. an
43
+ explicit connection URI:
44
+
45
+ ```bash
46
+ npx -y @reddb-io/mcp --uri file:///var/lib/reddb/agent.rdb
47
+ ```
48
+
49
+ ## Binary resolution
50
+
51
+ Resolution reuses RedDB's internal bin-resolver / asset-fetcher and follows
52
+ the same precedence as the SDK (PATH is never consulted, per ADR 0006):
53
+
54
+ 1. `REDDB_BIN` — absolute path to a `red` binary you provide. Returned verbatim.
55
+ 2. A binary already cached at `<package>/bin/red[.exe]`.
56
+ 3. Otherwise the matching release asset is downloaded from GitHub Releases.
57
+
58
+ ### Environment overrides
59
+
60
+ | Variable | Effect |
61
+ | ------------------- | --------------------------------------------------- |
62
+ | `REDDB_BIN` | Use this binary; skip all download logic. |
63
+ | `REDDB_MCP_URI` | Connection URI forwarded to `red mcp --uri`. |
64
+ | `REDDB_MCP_VERSION` | Pull a specific release tag instead of the default. |
65
+ | `REDDB_MCP_REPO` | Fetch from a fork (default: `reddb-io/reddb`). |
66
+
67
+ ## License
68
+
69
+ MIT
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@reddb-io/mcp",
3
+ "version": "1.16.0",
4
+ "description": "Zero-install npx launcher for the RedDB MCP surface — fetches the matching red binary and spawns `red mcp` over stdio.",
5
+ "type": "module",
6
+ "bin": {
7
+ "reddb-mcp": "./src/launcher.js"
8
+ },
9
+ "files": [
10
+ "src/",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "keywords": [
17
+ "reddb",
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "database",
21
+ "agent",
22
+ "npx",
23
+ "launcher"
24
+ ],
25
+ "license": "MIT",
26
+ "homepage": "https://github.com/reddb-io/reddb",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/reddb-io/reddb.git",
30
+ "directory": "packages/mcp"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/reddb-io/reddb/issues"
34
+ },
35
+ "scripts": {
36
+ "test": "node test/launcher.test.mjs"
37
+ }
38
+ }
package/src/binary.js ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Locate (or download) the `red` binary for the `@reddb-io/mcp` launcher.
3
+ *
4
+ * Resolution precedence (reuses the vendored bin-resolver — env > local,
5
+ * PATH is never consulted, per ADR 0006):
6
+ * 1. `REDDB_BIN` env var → returned verbatim.
7
+ * 2. `<package>/bin/red[.exe]` if already present (e.g. a previous npx run
8
+ * cached it, or a workspace checkout vendored it).
9
+ * 3. Otherwise download the matching GitHub Release asset (reusing the
10
+ * vendored asset-fetcher — no new download logic) into `<package>/bin/`
11
+ * and return that path.
12
+ *
13
+ * The download tag tracks this package's own version (`v<pkg.version>`),
14
+ * which the release flow keeps locked to the engine version. Because
15
+ * `npx -y @reddb-io/mcp` always pulls the latest published launcher, this
16
+ * yields the latest released engine with zero local install.
17
+ */
18
+
19
+ import { existsSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs'
20
+ import { fileURLToPath } from 'node:url'
21
+ import { dirname, resolve, join } from 'node:path'
22
+
23
+ import { resolveBin } from './internal/bin-resolver/index.js'
24
+ import { fetchReleaseAsset } from './internal/asset-fetcher/index.js'
25
+
26
+ const HERE = dirname(fileURLToPath(import.meta.url))
27
+ const PACKAGE_ROOT = resolve(HERE, '..')
28
+
29
+ export const DEFAULT_REPO = 'reddb-io/reddb'
30
+
31
+ /** Platform-specific base name of the engine binary. */
32
+ export function binaryName(platform = process.platform) {
33
+ return platform === 'win32' ? 'red.exe' : 'red'
34
+ }
35
+
36
+ /**
37
+ * Non-throwing probe reusing the vendored bin-resolver: returns an
38
+ * absolute path when `REDDB_BIN` is set or a local binary exists, else
39
+ * `null` (so the caller can decide to download).
40
+ *
41
+ * @param {{ packageRoot?: string, platform?: string }} [opts]
42
+ * @returns {string|null}
43
+ */
44
+ export function tryResolveBinary({ packageRoot = PACKAGE_ROOT, platform = process.platform } = {}) {
45
+ try {
46
+ return resolveBin({ name: binaryName(platform), packageRoot, envVar: 'REDDB_BIN' })
47
+ } catch {
48
+ return null
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Resolve the engine binary, downloading it from GitHub Releases when it is
54
+ * not already available. Returns the absolute path to a runnable binary.
55
+ *
56
+ * @param {{
57
+ * version: string,
58
+ * packageRoot?: string,
59
+ * platform?: string,
60
+ * arch?: string,
61
+ * repo?: string,
62
+ * fetchAsset?: typeof fetchReleaseAsset,
63
+ * }} opts
64
+ * @returns {Promise<string>}
65
+ */
66
+ export async function ensureBinary({
67
+ version,
68
+ packageRoot = PACKAGE_ROOT,
69
+ platform = process.platform,
70
+ arch = process.arch,
71
+ repo = process.env.REDDB_MCP_REPO || DEFAULT_REPO,
72
+ fetchAsset = fetchReleaseAsset,
73
+ } = {}) {
74
+ if (typeof version !== 'string' || version === '') {
75
+ throw new TypeError('ensureBinary: `version` must be a non-empty string')
76
+ }
77
+
78
+ const existing = tryResolveBinary({ packageRoot, platform })
79
+ if (existing) {
80
+ return existing
81
+ }
82
+
83
+ const tag = normalizeTag(process.env.REDDB_MCP_VERSION || version)
84
+ const body = await fetchAsset({ repo, tag, platform, arch, binName: 'red' })
85
+
86
+ const binDir = join(packageRoot, 'bin')
87
+ mkdirSync(binDir, { recursive: true })
88
+ const dest = join(binDir, binaryName(platform))
89
+ writeFileSync(dest, body)
90
+ if (platform !== 'win32') {
91
+ chmodSync(dest, 0o755)
92
+ }
93
+ return dest
94
+ }
95
+
96
+ function normalizeTag(value) {
97
+ const v = String(value).trim()
98
+ return v.startsWith('v') ? v : `v${v}`
99
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Resolve the GitHub release asset filename for a given Node
3
+ * `process.platform` / `process.arch` pair and binary base name.
4
+ *
5
+ * Mapping is the existing scheme from `drivers/js/postinstall.js`:
6
+ * linux + x64 → <bin>-linux-x86_64
7
+ * linux + arm64 → <bin>-linux-aarch64
8
+ * linux + arm/armv7l→ <bin>-linux-armv7
9
+ * darwin + x64 → <bin>-macos-x86_64
10
+ * darwin + arm64 → <bin>-macos-aarch64
11
+ * win32 + x64 → <bin>-windows-x86_64.exe
12
+ *
13
+ * Throws `UnsupportedPlatformError` for any other combination.
14
+ */
15
+
16
+ export class UnsupportedPlatformError extends Error {
17
+ constructor(platform, arch) {
18
+ super(`unsupported platform/arch combination: ${platform}/${arch}`)
19
+ this.name = 'UnsupportedPlatformError'
20
+ this.code = 'UNSUPPORTED_PLATFORM'
21
+ this.platform = platform
22
+ this.arch = arch
23
+ }
24
+ }
25
+
26
+ export function composeAssetName({ platform, arch, binName }) {
27
+ if (typeof binName !== 'string' || binName === '') {
28
+ throw new TypeError('composeAssetName: `binName` must be a non-empty string')
29
+ }
30
+ if (platform === 'linux' && arch === 'x64') return `${binName}-linux-x86_64`
31
+ if (platform === 'linux' && arch === 'arm64') return `${binName}-linux-aarch64`
32
+ if (platform === 'linux' && (arch === 'arm' || arch === 'armv7l')) return `${binName}-linux-armv7`
33
+ if (platform === 'darwin' && arch === 'x64') return `${binName}-macos-x86_64`
34
+ if (platform === 'darwin' && arch === 'arm64') return `${binName}-macos-aarch64`
35
+ if (platform === 'win32' && arch === 'x64') return `${binName}-windows-x86_64.exe`
36
+ throw new UnsupportedPlatformError(platform, arch)
37
+ }
@@ -0,0 +1,23 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ export class ChecksumMismatchError extends Error {
4
+ constructor(expected, actual) {
5
+ super(`checksum mismatch: expected sha256=${expected}, got sha256=${actual}`)
6
+ this.name = 'ChecksumMismatchError'
7
+ this.code = 'CHECKSUM_MISMATCH'
8
+ this.expected = expected
9
+ this.actual = actual
10
+ }
11
+ }
12
+
13
+ export function sha256Hex(buf) {
14
+ return createHash('sha256').update(buf).digest('hex')
15
+ }
16
+
17
+ export function verifySha256(buf, expected) {
18
+ const expectedNorm = String(expected).trim().toLowerCase()
19
+ const actual = sha256Hex(buf)
20
+ if (actual !== expectedNorm) {
21
+ throw new ChecksumMismatchError(expectedNorm, actual)
22
+ }
23
+ }
@@ -0,0 +1,89 @@
1
+ import { request as httpsRequest } from 'node:https'
2
+ import { request as httpRequest } from 'node:http'
3
+
4
+ const MAX_REDIRECTS = 5
5
+
6
+ export class AssetNotFoundError extends Error {
7
+ constructor(url) {
8
+ super(`asset not found (HTTP 404) at ${url}`)
9
+ this.name = 'AssetNotFoundError'
10
+ this.code = 'ASSET_NOT_FOUND'
11
+ this.url = url
12
+ }
13
+ }
14
+
15
+ export class HttpError extends Error {
16
+ constructor(status, url) {
17
+ super(`HTTP ${status} fetching ${url}`)
18
+ this.name = 'HttpError'
19
+ this.code = 'HTTP_ERROR'
20
+ this.status = status
21
+ this.url = url
22
+ }
23
+ }
24
+
25
+ export class TooManyRedirectsError extends Error {
26
+ constructor(url) {
27
+ super(`too many redirects (>${MAX_REDIRECTS}) starting at ${url}`)
28
+ this.name = 'TooManyRedirectsError'
29
+ this.code = 'TOO_MANY_REDIRECTS'
30
+ this.url = url
31
+ }
32
+ }
33
+
34
+ function pickRequest(url) {
35
+ return url.startsWith('http://') ? httpRequest : httpsRequest
36
+ }
37
+
38
+ function resolveLocation(currentUrl, location) {
39
+ if (/^https?:\/\//i.test(location)) return location
40
+ return new URL(location, currentUrl).toString()
41
+ }
42
+
43
+ export function downloadFollowingRedirects(url, { userAgent, originalUrl } = {}, depth = 0) {
44
+ const startUrl = originalUrl || url
45
+ if (depth > MAX_REDIRECTS) {
46
+ return Promise.reject(new TooManyRedirectsError(startUrl))
47
+ }
48
+ const request = pickRequest(url)
49
+ return new Promise((resolve, reject) => {
50
+ const req = request(
51
+ url,
52
+ {
53
+ method: 'GET',
54
+ headers: {
55
+ 'User-Agent': userAgent || 'reddb-internal-asset-fetcher',
56
+ Accept: 'application/octet-stream',
57
+ },
58
+ },
59
+ (res) => {
60
+ const status = res.statusCode || 0
61
+ if (status >= 300 && status < 400 && res.headers.location) {
62
+ res.resume()
63
+ const next = resolveLocation(url, res.headers.location)
64
+ downloadFollowingRedirects(next, { userAgent, originalUrl: startUrl }, depth + 1).then(
65
+ resolve,
66
+ reject,
67
+ )
68
+ return
69
+ }
70
+ if (status === 404) {
71
+ res.resume()
72
+ reject(new AssetNotFoundError(startUrl))
73
+ return
74
+ }
75
+ if (status < 200 || status >= 300) {
76
+ res.resume()
77
+ reject(new HttpError(status, url))
78
+ return
79
+ }
80
+ const chunks = []
81
+ res.on('data', (chunk) => chunks.push(chunk))
82
+ res.on('end', () => resolve(Buffer.concat(chunks)))
83
+ res.on('error', reject)
84
+ },
85
+ )
86
+ req.on('error', reject)
87
+ req.end()
88
+ })
89
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @reddb-io/internal-asset-fetcher — fetch a `red`/`red_client` binary
3
+ * from a GitHub release.
4
+ *
5
+ * Public surface: one function.
6
+ *
7
+ * fetchReleaseAsset({ repo, tag, platform, arch, binName, sha256? }) → Buffer
8
+ *
9
+ * Steps:
10
+ * 1. Map (platform, arch, binName) → asset filename.
11
+ * 2. Compose the GitHub download URL: `https://github.com/<repo>/releases/download/<tag>/<asset>`.
12
+ * 3. Follow up to 5 redirects, returning the final body as a Buffer.
13
+ * 4. If `sha256` was supplied, verify before returning.
14
+ *
15
+ * Errors carry distinct `.code` values so callers can differentiate:
16
+ * - UNSUPPORTED_PLATFORM — no asset for this platform/arch
17
+ * - ASSET_NOT_FOUND — HTTP 404 (release/tag/asset name wrong)
18
+ * - CHECKSUM_MISMATCH — body downloaded but sha256 mismatched
19
+ * - HTTP_ERROR — any other non-2xx status
20
+ * - TOO_MANY_REDIRECTS — redirect chain longer than 5 hops
21
+ *
22
+ * Internal modules (`./asset-name.js`, `./download.js`, `./checksum.js`)
23
+ * are not part of the public contract — only `fetchReleaseAsset` is.
24
+ * They are imported directly in tests for focused coverage.
25
+ */
26
+
27
+ import { composeAssetName } from './asset-name.js'
28
+ import { downloadFollowingRedirects } from './download.js'
29
+ import { verifySha256 } from './checksum.js'
30
+
31
+ export async function fetchReleaseAsset({ repo, tag, platform, arch, binName, sha256 } = {}) {
32
+ if (typeof repo !== 'string' || repo === '') {
33
+ throw new TypeError('fetchReleaseAsset: `repo` must be a non-empty string (e.g. "reddb-io/reddb")')
34
+ }
35
+ if (typeof tag !== 'string' || tag === '') {
36
+ throw new TypeError('fetchReleaseAsset: `tag` must be a non-empty string (e.g. "v0.2.9")')
37
+ }
38
+ if (typeof platform !== 'string' || platform === '') {
39
+ throw new TypeError('fetchReleaseAsset: `platform` must be a non-empty string')
40
+ }
41
+ if (typeof arch !== 'string' || arch === '') {
42
+ throw new TypeError('fetchReleaseAsset: `arch` must be a non-empty string')
43
+ }
44
+
45
+ const assetName = composeAssetName({ platform, arch, binName })
46
+ const url = `https://github.com/${repo}/releases/download/${tag}/${assetName}`
47
+ const body = await downloadFollowingRedirects(url)
48
+ if (sha256) {
49
+ verifySha256(body, sha256)
50
+ }
51
+ return body
52
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @reddb-io/internal-bin-resolver — runtime lookup for a pinned binary.
3
+ *
4
+ * Precedence:
5
+ * 1. `process.env[envVar]` if set and non-empty → returned verbatim.
6
+ * No existence probe: the env var is the user's "I know what I'm
7
+ * doing" override.
8
+ * 2. `<packageRoot>/bin/<name>` if it exists.
9
+ * 3. Otherwise throw an actionable error naming the env var, the
10
+ * expected local path, and a one-line `pnpm install` hint.
11
+ *
12
+ * `PATH` is deliberately never consulted. SDK/client wire formats are
13
+ * version-coupled to the binary; silent fallback to a stale `PATH`
14
+ * binary fails as misframed RPC, not "command not found". See ADR 0006.
15
+ */
16
+
17
+ import { existsSync } from 'node:fs'
18
+ import { join } from 'node:path'
19
+
20
+ /**
21
+ * @param {{ name: string, packageRoot: string, envVar: string }} opts
22
+ * @returns {string} absolute path to the binary
23
+ * @throws {Error} when neither env override nor local binary is usable
24
+ */
25
+ export function resolveBin(opts) {
26
+ if (!opts || typeof opts !== 'object') {
27
+ throw new TypeError('resolveBin: options object required')
28
+ }
29
+ const { name, packageRoot, envVar } = opts
30
+ if (typeof name !== 'string' || name === '') {
31
+ throw new TypeError('resolveBin: `name` must be a non-empty string')
32
+ }
33
+ if (typeof packageRoot !== 'string' || packageRoot === '') {
34
+ throw new TypeError('resolveBin: `packageRoot` must be a non-empty string')
35
+ }
36
+ if (typeof envVar !== 'string' || envVar === '') {
37
+ throw new TypeError('resolveBin: `envVar` must be a non-empty string')
38
+ }
39
+
40
+ const override = process.env?.[envVar]
41
+ if (typeof override === 'string' && override !== '') {
42
+ return override
43
+ }
44
+
45
+ const local = join(packageRoot, 'bin', name)
46
+ if (existsSync(local)) {
47
+ return local
48
+ }
49
+
50
+ throw new Error(
51
+ `reddb: binary "${name}" not found.\n` +
52
+ ` expected at: ${local}\n` +
53
+ ` override: set ${envVar}=/path/to/${name}\n` +
54
+ ` fix: re-run \`pnpm install\` (the postinstall script downloads it),\n` +
55
+ ` or check the postinstall log for a download error.`,
56
+ )
57
+ }
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @reddb-io/mcp — zero-install npx launcher for the RedDB MCP surface.
4
+ *
5
+ * npx -y @reddb-io/mcp [args…]
6
+ *
7
+ * Resolves (or downloads) the matching `red` binary and execs `red mcp`
8
+ * over stdio. No tool/knowledge logic lives here — the launcher only
9
+ * fetches + spawns the native engine, so the MCP surface is always the
10
+ * latest released engine's.
11
+ */
12
+
13
+ import { createRequire } from 'node:module'
14
+
15
+ import { ensureBinary, DEFAULT_REPO } from './binary.js'
16
+ import { spawnMcp } from './spawn.js'
17
+
18
+ const require = createRequire(import.meta.url)
19
+ const pkg = require('../package.json')
20
+
21
+ ensureBinary({ version: pkg.version })
22
+ .then((binary) => {
23
+ const child = spawnMcp(binary, process.argv.slice(2))
24
+ child.on('error', (err) => {
25
+ process.stderr.write(`reddb-mcp: failed to spawn red (${err.message})\n`)
26
+ process.exit(1)
27
+ })
28
+ child.on('exit', (code, signal) => {
29
+ process.exit(signal ? 1 : (code ?? 0))
30
+ })
31
+ })
32
+ .catch((err) => {
33
+ process.stderr.write(formatFailure(err))
34
+ process.exit(1)
35
+ })
36
+
37
+ function formatFailure(err) {
38
+ const repo = process.env.REDDB_MCP_REPO || DEFAULT_REPO
39
+ const escapeHatches =
40
+ ` Escape hatches:\n` +
41
+ ` - Provide the binary yourself: export REDDB_BIN=/path/to/red\n` +
42
+ ` - Install red via the official installer:\n` +
43
+ ` curl -fsSL https://raw.githubusercontent.com/${repo}/main/install.sh | bash\n` +
44
+ ` export REDDB_BIN="$(command -v red)"\n` +
45
+ ` - Download a release binary manually from\n` +
46
+ ` https://github.com/${repo}/releases\n`
47
+
48
+ if (err && err.code === 'UNSUPPORTED_PLATFORM') {
49
+ return (
50
+ `reddb-mcp: no prebuilt red binary for ${process.platform}/${process.arch}.\n` +
51
+ ` Build from source: https://github.com/${repo}#build-from-source\n` +
52
+ escapeHatches
53
+ )
54
+ }
55
+ if (err && err.code === 'ASSET_NOT_FOUND') {
56
+ return (
57
+ `reddb-mcp: release asset not found${err.url ? ` at ${err.url}` : ''}.\n` +
58
+ ` The GitHub Release for this launcher version may not be published yet,\n` +
59
+ ` or your platform's binary was not produced for that release.\n` +
60
+ escapeHatches
61
+ )
62
+ }
63
+ return (
64
+ `reddb-mcp: could not obtain the red binary (${(err && err.message) || err}).\n` +
65
+ escapeHatches
66
+ )
67
+ }
package/src/spawn.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Spawn `red mcp` over stdio.
3
+ *
4
+ * The launcher reimplements no tool/knowledge logic — it simply execs the
5
+ * native engine's `mcp` subcommand and inherits stdio so the MCP JSON-RPC
6
+ * stream flows straight through to the parent (the agent host). Any extra
7
+ * argv passed to `npx @reddb-io/mcp ...` is forwarded after `mcp` (e.g.
8
+ * `--uri <addr>` for connection mode selection).
9
+ *
10
+ * `spawn` is injectable so tests can assert the binary, argv, and stdio
11
+ * wiring without launching a real process.
12
+ */
13
+
14
+ import { spawn as nodeSpawn } from 'node:child_process'
15
+
16
+ /**
17
+ * @param {string} binary absolute path to the `red` binary
18
+ * @param {string[]} [extraArgs] argv forwarded after the `mcp` subcommand
19
+ * @param {{
20
+ * spawn?: typeof nodeSpawn,
21
+ * stdio?: import('node:child_process').StdioOptions,
22
+ * env?: NodeJS.ProcessEnv,
23
+ * cwd?: string,
24
+ * }} [opts]
25
+ * @returns {import('node:child_process').ChildProcess}
26
+ */
27
+ export function spawnMcp(
28
+ binary,
29
+ extraArgs = [],
30
+ { spawn = nodeSpawn, stdio = 'inherit', env = process.env, cwd = process.cwd() } = {},
31
+ ) {
32
+ if (typeof binary !== 'string' || binary === '') {
33
+ throw new TypeError('spawnMcp: `binary` must be a non-empty string')
34
+ }
35
+ return spawn(binary, redMcpArgs(extraArgs, env), { stdio, env, cwd })
36
+ }
37
+
38
+ export function redMcpArgs(extraArgs = [], env = process.env) {
39
+ if (hasFlag(extraArgs, 'uri')) {
40
+ return ['mcp', ...extraArgs]
41
+ }
42
+
43
+ const uri = env.REDDB_MCP_URI
44
+ if (typeof uri === 'string' && uri !== '') {
45
+ return ['mcp', '--uri', uri, ...extraArgs]
46
+ }
47
+
48
+ return ['mcp', ...extraArgs]
49
+ }
50
+
51
+ function hasFlag(args, name) {
52
+ const long = `--${name}`
53
+ return args.some((arg) => arg === long || arg.startsWith(`${long}=`))
54
+ }