@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 +111 -0
- package/README.md +69 -0
- package/package.json +38 -0
- package/src/binary.js +99 -0
- package/src/internal/asset-fetcher/asset-name.js +37 -0
- package/src/internal/asset-fetcher/checksum.js +23 -0
- package/src/internal/asset-fetcher/download.js +89 -0
- package/src/internal/asset-fetcher/index.js +52 -0
- package/src/internal/bin-resolver/index.js +57 -0
- package/src/launcher.js +67 -0
- package/src/spawn.js +54 -0
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
|
+
}
|
package/src/launcher.js
ADDED
|
@@ -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
|
+
}
|