@andespindola/brainlink 0.1.0-beta.166 → 0.1.0-beta.168
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 +98 -0
- package/dist/cli/commands/vault-commands.js +182 -0
- package/dist/cli/commands/write-commands.js +33 -0
- package/dist/cli/main.js +2 -0
- package/dist/mcp/http-server.js +97 -0
- package/docs/AGENT_USAGE.md +27 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -717,6 +717,21 @@ When the configured default vault is changed manually in config files, Brainlink
|
|
|
717
717
|
Use `--global` to write to `$BRAINLINK_HOME/brainlink.config.json`, `--no-migrate` to skip migration, and `--no-index` to skip post-migration indexing.
|
|
718
718
|
`config doctor` is dry-run by default; use `--fix` to apply safe config normalization and allowlist fixes.
|
|
719
719
|
|
|
720
|
+
### `vaults`
|
|
721
|
+
|
|
722
|
+
```bash
|
|
723
|
+
blink vaults list
|
|
724
|
+
blink vaults list --json
|
|
725
|
+
blink vaults use /absolute/path/to/vault
|
|
726
|
+
blink vaults use /absolute/path/to/vault --global
|
|
727
|
+
blink vaults delete /absolute/path/to/vault --yes
|
|
728
|
+
blink vaults delete /absolute/path/to/vault --yes --prune-config
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
Lists known vaults from the configured default, `allowedVaults`, and the built-in default at `$HOME/.brainlink/vault`.
|
|
732
|
+
`vaults use` chooses the default vault without migrating memory; use `migrate-vault` or `config set-vault --migrate-from` when you want to copy Markdown memory between vaults.
|
|
733
|
+
`vaults delete` only deletes local filesystem vaults, requires `--yes`, refuses bucket vaults, and refuses deleting the current default vault. Choose another default first with `vaults use`.
|
|
734
|
+
|
|
720
735
|
### `migrate-vault`
|
|
721
736
|
|
|
722
737
|
```bash
|
|
@@ -1049,6 +1064,89 @@ If no `vault` is configured and no `--vault` flag is passed, Brainlink uses `$HO
|
|
|
1049
1064
|
`autoIndexOnWrite` is optional and defaults to `true`. Set it to `false` to defer indexing after writes.
|
|
1050
1065
|
`autoCanonicalContextLinks` is optional and defaults to `true`. When enabled, `blink add`, `brainlink_add_note` and `brainlink_add_file` add a canonical `## Context Links` entry to the inferred context hub, creating that hub when needed.
|
|
1051
1066
|
|
|
1067
|
+
## Remote MCP Server
|
|
1068
|
+
|
|
1069
|
+
Brainlink can run as a centralized MCP service for clustered workloads. This keeps one shared memory service inside the cluster while applications and agents connect to the MCP endpoint over Streamable HTTP.
|
|
1070
|
+
|
|
1071
|
+
```bash
|
|
1072
|
+
BRAINLINK_MCP_TOKEN="change-me" brainlink mcp-server \
|
|
1073
|
+
--vault /data/vault \
|
|
1074
|
+
--host 0.0.0.0 \
|
|
1075
|
+
--port 3333 \
|
|
1076
|
+
--path /mcp
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
The server exposes:
|
|
1080
|
+
|
|
1081
|
+
```txt
|
|
1082
|
+
POST /mcp MCP Streamable HTTP endpoint
|
|
1083
|
+
GET /healthz liveness probe
|
|
1084
|
+
GET /readyz readiness probe
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
When `BRAINLINK_MCP_TOKEN` or `--token` is set, MCP requests must include:
|
|
1088
|
+
|
|
1089
|
+
```txt
|
|
1090
|
+
Authorization: Bearer <token>
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
For Kubernetes, run Brainlink as a central `Deployment` with a `Service` and a mounted vault volume:
|
|
1094
|
+
|
|
1095
|
+
```yaml
|
|
1096
|
+
apiVersion: apps/v1
|
|
1097
|
+
kind: Deployment
|
|
1098
|
+
metadata:
|
|
1099
|
+
name: brainlink-mcp
|
|
1100
|
+
spec:
|
|
1101
|
+
replicas: 1
|
|
1102
|
+
selector:
|
|
1103
|
+
matchLabels:
|
|
1104
|
+
app: brainlink-mcp
|
|
1105
|
+
template:
|
|
1106
|
+
metadata:
|
|
1107
|
+
labels:
|
|
1108
|
+
app: brainlink-mcp
|
|
1109
|
+
spec:
|
|
1110
|
+
containers:
|
|
1111
|
+
- name: brainlink
|
|
1112
|
+
image: brainlink:latest
|
|
1113
|
+
args: ["brainlink", "mcp-server", "--vault", "/data/vault", "--host", "0.0.0.0", "--port", "3333"]
|
|
1114
|
+
env:
|
|
1115
|
+
- name: BRAINLINK_MCP_TOKEN
|
|
1116
|
+
valueFrom:
|
|
1117
|
+
secretKeyRef:
|
|
1118
|
+
name: brainlink-mcp
|
|
1119
|
+
key: token
|
|
1120
|
+
ports:
|
|
1121
|
+
- containerPort: 3333
|
|
1122
|
+
readinessProbe:
|
|
1123
|
+
httpGet:
|
|
1124
|
+
path: /readyz
|
|
1125
|
+
port: 3333
|
|
1126
|
+
livenessProbe:
|
|
1127
|
+
httpGet:
|
|
1128
|
+
path: /healthz
|
|
1129
|
+
port: 3333
|
|
1130
|
+
volumeMounts:
|
|
1131
|
+
- name: vault
|
|
1132
|
+
mountPath: /data/vault
|
|
1133
|
+
volumes:
|
|
1134
|
+
- name: vault
|
|
1135
|
+
persistentVolumeClaim:
|
|
1136
|
+
claimName: brainlink-vault
|
|
1137
|
+
---
|
|
1138
|
+
apiVersion: v1
|
|
1139
|
+
kind: Service
|
|
1140
|
+
metadata:
|
|
1141
|
+
name: brainlink-mcp
|
|
1142
|
+
spec:
|
|
1143
|
+
selector:
|
|
1144
|
+
app: brainlink-mcp
|
|
1145
|
+
ports:
|
|
1146
|
+
- port: 3333
|
|
1147
|
+
targetPort: 3333
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1052
1150
|
Use `"embeddingProvider": "none"` when you want FTS-only indexing.
|
|
1053
1151
|
|
|
1054
1152
|
For local security checks, set your Snyk token in the environment:
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { extname, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { doctorVault } from '../../application/analyze-vault.js';
|
|
4
|
+
import { defaultBrainlinkConfig, loadBrainlinkConfig, loadRawConfig, writeRawConfig } from '../../infrastructure/config.js';
|
|
5
|
+
import { assertVaultAllowed, isBucketVaultPath, resolveVaultPath } from '../../infrastructure/file-system-vault.js';
|
|
6
|
+
import { print } from '../runtime.js';
|
|
7
|
+
const excludedDirectories = new Set(['.git', 'node_modules', 'dist']);
|
|
8
|
+
const resolveScope = (globalOption) => globalOption ? 'global' : 'local';
|
|
9
|
+
const normalizeVaultPath = (vault) => assertVaultAllowed(vault, []);
|
|
10
|
+
const uniqueValues = (values) => Array.from(new Set(values));
|
|
11
|
+
const sameVault = (left, right) => normalizeVaultPath(left) === normalizeVaultPath(right);
|
|
12
|
+
const isDirectory = async (path) => {
|
|
13
|
+
try {
|
|
14
|
+
return (await stat(path)).isDirectory();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const countMarkdownFiles = async (directory) => {
|
|
21
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
22
|
+
const counts = await Promise.all(entries.map(async (entry) => {
|
|
23
|
+
const absolutePath = join(directory, entry.name);
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
return excludedDirectories.has(entry.name) ? 0 : countMarkdownFiles(absolutePath);
|
|
26
|
+
}
|
|
27
|
+
return entry.isFile() && extname(entry.name).toLowerCase() === '.md' ? 1 : 0;
|
|
28
|
+
}));
|
|
29
|
+
return counts.reduce((total, count) => total + count, 0);
|
|
30
|
+
};
|
|
31
|
+
const readIndexedDocumentCount = async (vaultPath) => {
|
|
32
|
+
try {
|
|
33
|
+
const raw = await readFile(join(vaultPath, '.brainlink', 'index.json'), 'utf8');
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
return Array.isArray(parsed.documents) ? parsed.documents.length : null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const addCandidate = (state, path, source) => {
|
|
42
|
+
const normalized = normalizeVaultPath(path);
|
|
43
|
+
const current = state.get(normalized);
|
|
44
|
+
return new Map(state).set(normalized, {
|
|
45
|
+
path: normalized,
|
|
46
|
+
sources: uniqueValues([...(current?.sources ?? []), source])
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
const listVaultEntries = async () => {
|
|
50
|
+
const config = await loadBrainlinkConfig();
|
|
51
|
+
const candidates = [config.vault, ...config.allowedVaults, defaultBrainlinkConfig.vault].reduce((state, path, index) => addCandidate(state, path, index === 0 ? 'configured' : path === defaultBrainlinkConfig.vault ? 'default' : 'allowed'), new Map());
|
|
52
|
+
const entries = await Promise.all(Array.from(candidates.values()).map(async (candidate) => {
|
|
53
|
+
if (isBucketVaultPath(candidate.path)) {
|
|
54
|
+
return {
|
|
55
|
+
path: candidate.path,
|
|
56
|
+
sources: candidate.sources,
|
|
57
|
+
current: sameVault(candidate.path, config.vault),
|
|
58
|
+
kind: 'bucket',
|
|
59
|
+
exists: null,
|
|
60
|
+
markdownCount: null,
|
|
61
|
+
indexedDocumentCount: null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const path = resolveVaultPath(candidate.path);
|
|
65
|
+
const exists = await isDirectory(path);
|
|
66
|
+
return {
|
|
67
|
+
path,
|
|
68
|
+
sources: candidate.sources,
|
|
69
|
+
current: sameVault(path, config.vault),
|
|
70
|
+
kind: 'local',
|
|
71
|
+
exists,
|
|
72
|
+
markdownCount: exists ? await countMarkdownFiles(path) : null,
|
|
73
|
+
indexedDocumentCount: exists ? await readIndexedDocumentCount(path) : null
|
|
74
|
+
};
|
|
75
|
+
}));
|
|
76
|
+
return entries.sort((left, right) => Number(right.current) - Number(left.current) || left.path.localeCompare(right.path));
|
|
77
|
+
};
|
|
78
|
+
const removeVaultFromAllowedVaults = async (vaultPath) => {
|
|
79
|
+
const scopes = ['local', 'global'];
|
|
80
|
+
await Promise.all(scopes.map(async (scope) => {
|
|
81
|
+
const rawConfig = await loadRawConfig(scope);
|
|
82
|
+
const allowedVaults = Array.isArray(rawConfig.allowedVaults)
|
|
83
|
+
? rawConfig.allowedVaults.filter((path) => typeof path === 'string')
|
|
84
|
+
: [];
|
|
85
|
+
const nextAllowedVaults = allowedVaults.filter((path) => !sameVault(path, vaultPath));
|
|
86
|
+
if (nextAllowedVaults.length === allowedVaults.length) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await writeRawConfig(scope, {
|
|
90
|
+
...rawConfig,
|
|
91
|
+
allowedVaults: nextAllowedVaults
|
|
92
|
+
});
|
|
93
|
+
}));
|
|
94
|
+
};
|
|
95
|
+
export const registerVaultCommands = (program) => {
|
|
96
|
+
const vaultsCommand = program.command('vaults').description('list, choose and delete Brainlink vaults');
|
|
97
|
+
vaultsCommand
|
|
98
|
+
.command('list')
|
|
99
|
+
.option('--json', 'print machine-readable JSON')
|
|
100
|
+
.description('list known Brainlink vaults from config and defaults')
|
|
101
|
+
.action(async (options) => {
|
|
102
|
+
const config = await loadBrainlinkConfig();
|
|
103
|
+
const vaults = await listVaultEntries();
|
|
104
|
+
print(options.json, {
|
|
105
|
+
currentVault: config.vault,
|
|
106
|
+
vaults
|
|
107
|
+
}, () => vaults
|
|
108
|
+
.map((vault) => {
|
|
109
|
+
const status = vault.exists === null ? 'remote' : vault.exists ? 'exists' : 'missing';
|
|
110
|
+
const counts = vault.markdownCount === null
|
|
111
|
+
? ''
|
|
112
|
+
: ` markdown=${vault.markdownCount} indexed=${vault.indexedDocumentCount ?? 'unknown'}`;
|
|
113
|
+
return `${vault.current ? '* ' : ' '}${vault.path} [${vault.sources.join(',')}; ${status}; ${vault.kind}]${counts}`;
|
|
114
|
+
})
|
|
115
|
+
.join('\n'));
|
|
116
|
+
});
|
|
117
|
+
vaultsCommand
|
|
118
|
+
.command('use <vault>')
|
|
119
|
+
.option('--global', 'write to global config in $BRAINLINK_HOME/brainlink.config.json')
|
|
120
|
+
.option('--no-allowlist', 'do not append the vault to allowedVaults in the target config file')
|
|
121
|
+
.option('--json', 'print machine-readable JSON')
|
|
122
|
+
.description('choose the default Brainlink vault without migrating memory')
|
|
123
|
+
.action(async (vault, options) => {
|
|
124
|
+
const scope = resolveScope(options.global);
|
|
125
|
+
const before = await loadBrainlinkConfig();
|
|
126
|
+
const targetVault = normalizeVaultPath(vault);
|
|
127
|
+
const rawConfig = await loadRawConfig(scope);
|
|
128
|
+
const shouldAllowlist = options.allowlist !== false;
|
|
129
|
+
const allowedVaults = Array.isArray(rawConfig.allowedVaults)
|
|
130
|
+
? rawConfig.allowedVaults.filter((path) => typeof path === 'string')
|
|
131
|
+
: [];
|
|
132
|
+
const nextAllowedVaults = shouldAllowlist ? uniqueValues([...allowedVaults, targetVault]) : allowedVaults;
|
|
133
|
+
const configPath = await writeRawConfig(scope, {
|
|
134
|
+
...rawConfig,
|
|
135
|
+
vault: targetVault,
|
|
136
|
+
allowedVaults: nextAllowedVaults
|
|
137
|
+
});
|
|
138
|
+
const doctor = await doctorVault(targetVault);
|
|
139
|
+
print(options.json, {
|
|
140
|
+
scope,
|
|
141
|
+
configPath,
|
|
142
|
+
previousVault: before.vault,
|
|
143
|
+
vault: targetVault,
|
|
144
|
+
doctor
|
|
145
|
+
}, () => `Default ${scope} vault set to ${targetVault} in ${configPath}.`);
|
|
146
|
+
});
|
|
147
|
+
vaultsCommand
|
|
148
|
+
.command('delete <vault>')
|
|
149
|
+
.option('--yes', 'confirm destructive vault deletion')
|
|
150
|
+
.option('--prune-config', 'remove the vault from allowedVaults after deletion')
|
|
151
|
+
.option('--json', 'print machine-readable JSON')
|
|
152
|
+
.description('delete a local filesystem vault after explicit confirmation')
|
|
153
|
+
.action(async (vault, options) => {
|
|
154
|
+
const config = await loadBrainlinkConfig();
|
|
155
|
+
const targetVault = normalizeVaultPath(vault);
|
|
156
|
+
if (isBucketVaultPath(targetVault)) {
|
|
157
|
+
throw new Error('Refusing to delete bucket vaults from the CLI. Remove bucket data with your storage provider tooling.');
|
|
158
|
+
}
|
|
159
|
+
const absoluteVault = resolveVaultPath(targetVault);
|
|
160
|
+
if (!isAbsolute(absoluteVault)) {
|
|
161
|
+
throw new Error(`Refusing to delete non-absolute vault path: ${absoluteVault}`);
|
|
162
|
+
}
|
|
163
|
+
if (sameVault(absoluteVault, config.vault)) {
|
|
164
|
+
throw new Error('Refusing to delete the current default vault. Choose another default with `blink vaults use <vault>` first.');
|
|
165
|
+
}
|
|
166
|
+
if (options.yes !== true) {
|
|
167
|
+
throw new Error('Refusing to delete vault without --yes.');
|
|
168
|
+
}
|
|
169
|
+
const existed = await isDirectory(absoluteVault);
|
|
170
|
+
if (existed) {
|
|
171
|
+
await rm(absoluteVault, { recursive: true, force: false });
|
|
172
|
+
}
|
|
173
|
+
if (options.pruneConfig) {
|
|
174
|
+
await removeVaultFromAllowedVaults(absoluteVault);
|
|
175
|
+
}
|
|
176
|
+
print(options.json, {
|
|
177
|
+
vault: absoluteVault,
|
|
178
|
+
deleted: existed,
|
|
179
|
+
prunedConfig: options.pruneConfig === true
|
|
180
|
+
}, () => `${existed ? 'Deleted' : 'Vault did not exist'}: ${absoluteVault}`);
|
|
181
|
+
});
|
|
182
|
+
};
|
|
@@ -20,6 +20,7 @@ import { loadBrainlinkConfig } from '../../infrastructure/config.js';
|
|
|
20
20
|
import { assertVaultAllowed, ensureVault, isBucketVaultPath } from '../../infrastructure/file-system-vault.js';
|
|
21
21
|
import { getBootstrapPolicy, getBootstrapSessionStatus, touchBootstrapSession } from '../../infrastructure/session-state.js';
|
|
22
22
|
import { addVolatileMemory, clearVolatileMemory } from '../../infrastructure/volatile-memory.js';
|
|
23
|
+
import { startRemoteMcpServer } from '../../mcp/http-server.js';
|
|
23
24
|
import { installAgentIntegration } from './agent-commands.js';
|
|
24
25
|
import { parsePositiveInteger, print, resolveOptions } from '../runtime.js';
|
|
25
26
|
const resolveAddContent = (options) => {
|
|
@@ -1105,6 +1106,38 @@ export const registerWriteCommands = (program) => {
|
|
|
1105
1106
|
? ' (auto-open disabled)'
|
|
1106
1107
|
: ''}`);
|
|
1107
1108
|
});
|
|
1109
|
+
program
|
|
1110
|
+
.command('mcp-server')
|
|
1111
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
1112
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
1113
|
+
.option('-h, --host <host>', 'remote MCP server host', '0.0.0.0')
|
|
1114
|
+
.option('-p, --port <port>', 'remote MCP server port', '3333')
|
|
1115
|
+
.option('--path <path>', 'remote MCP endpoint path', '/mcp')
|
|
1116
|
+
.option('--token <token>', 'bearer token required for MCP requests')
|
|
1117
|
+
.option('--no-index', 'skip indexing before starting the MCP server')
|
|
1118
|
+
.option('--json', 'print machine-readable JSON')
|
|
1119
|
+
.description('start a remote MCP server for centralized cluster access')
|
|
1120
|
+
.action(async (options) => {
|
|
1121
|
+
const resolved = await resolveOptions(options);
|
|
1122
|
+
const token = options.token ?? process.env.BRAINLINK_MCP_TOKEN;
|
|
1123
|
+
const server = await startRemoteMcpServer({
|
|
1124
|
+
vaultPath: resolved.vault,
|
|
1125
|
+
agent: resolved.agent,
|
|
1126
|
+
host: options.host ?? '0.0.0.0',
|
|
1127
|
+
port: parsePositiveInteger(options.port ?? '3333', 3333),
|
|
1128
|
+
path: options.path ?? '/mcp',
|
|
1129
|
+
token,
|
|
1130
|
+
shouldIndex: options.index
|
|
1131
|
+
});
|
|
1132
|
+
print(options.json, {
|
|
1133
|
+
url: server.url,
|
|
1134
|
+
healthUrl: server.healthUrl,
|
|
1135
|
+
readyUrl: server.readyUrl,
|
|
1136
|
+
vault: resolved.vault,
|
|
1137
|
+
agent: resolved.agent ?? '*',
|
|
1138
|
+
auth: token === undefined ? 'disabled' : 'bearer'
|
|
1139
|
+
}, () => `Brainlink remote MCP server running at ${server.url} (health: ${server.healthUrl}, readiness: ${server.readyUrl})`);
|
|
1140
|
+
});
|
|
1108
1141
|
program
|
|
1109
1142
|
.command('quickstart')
|
|
1110
1143
|
.option('-v, --vault <vault>', 'vault directory')
|
package/dist/cli/main.js
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { registerAgentCommands } from './commands/agent-commands.js';
|
|
7
7
|
import { registerConfigCommands } from './commands/config-commands.js';
|
|
8
8
|
import { registerReadCommands } from './commands/read-commands.js';
|
|
9
|
+
import { registerVaultCommands } from './commands/vault-commands.js';
|
|
9
10
|
import { registerWriteCommands } from './commands/write-commands.js';
|
|
10
11
|
const readPackageVersion = () => {
|
|
11
12
|
const packagePath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
|
|
@@ -24,6 +25,7 @@ program
|
|
|
24
25
|
registerWriteCommands(program);
|
|
25
26
|
registerReadCommands(program);
|
|
26
27
|
registerConfigCommands(program);
|
|
28
|
+
registerVaultCommands(program);
|
|
27
29
|
registerAgentCommands(program);
|
|
28
30
|
program.parseAsync().catch((error) => {
|
|
29
31
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { indexVault } from '../application/index-vault.js';
|
|
4
|
+
import { createBrainlinkMcpServer } from './server.js';
|
|
5
|
+
import { runStartupBootstrap } from './startup.js';
|
|
6
|
+
const normalizePath = (path) => {
|
|
7
|
+
const trimmed = path.trim();
|
|
8
|
+
if (trimmed.length === 0) {
|
|
9
|
+
return '/mcp';
|
|
10
|
+
}
|
|
11
|
+
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
12
|
+
};
|
|
13
|
+
const writeJson = (response, statusCode, value) => {
|
|
14
|
+
response.writeHead(statusCode, {
|
|
15
|
+
'content-type': 'application/json; charset=utf-8',
|
|
16
|
+
'cache-control': 'no-store'
|
|
17
|
+
});
|
|
18
|
+
response.end(`${JSON.stringify(value)}\n`);
|
|
19
|
+
};
|
|
20
|
+
const readBearerToken = (authorization) => {
|
|
21
|
+
const value = Array.isArray(authorization) ? authorization[0] : authorization;
|
|
22
|
+
const match = value?.match(/^Bearer\s+(.+)$/i);
|
|
23
|
+
return match?.[1]?.trim();
|
|
24
|
+
};
|
|
25
|
+
const isAuthorized = (expectedToken, authorization) => expectedToken === undefined || readBearerToken(authorization) === expectedToken;
|
|
26
|
+
export const startRemoteMcpServer = async (input) => {
|
|
27
|
+
const mcpPath = normalizePath(input.path);
|
|
28
|
+
const startup = await runStartupBootstrap();
|
|
29
|
+
if (input.shouldIndex) {
|
|
30
|
+
await indexVault(input.vaultPath);
|
|
31
|
+
}
|
|
32
|
+
if (startup.error) {
|
|
33
|
+
console.error(`Brainlink MCP startup bootstrap warning: ${startup.error}`);
|
|
34
|
+
}
|
|
35
|
+
const server = createServer(async (request, response) => {
|
|
36
|
+
const url = new URL(request.url ?? '/', `http://${request.headers.host ?? input.host}`);
|
|
37
|
+
if (url.pathname === '/healthz') {
|
|
38
|
+
writeJson(response, 200, { ok: true });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (url.pathname === '/readyz') {
|
|
42
|
+
writeJson(response, startup.error ? 503 : 200, {
|
|
43
|
+
ok: !startup.error,
|
|
44
|
+
vault: input.vaultPath,
|
|
45
|
+
agent: input.agent ?? startup.agent ?? '*',
|
|
46
|
+
...(startup.error ? { error: startup.error } : {})
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (url.pathname !== mcpPath) {
|
|
51
|
+
writeJson(response, 404, { error: 'Not found' });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!isAuthorized(input.token, request.headers.authorization)) {
|
|
55
|
+
writeJson(response, 401, { error: 'Unauthorized' });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const mcpServer = createBrainlinkMcpServer();
|
|
60
|
+
const transport = new StreamableHTTPServerTransport({
|
|
61
|
+
sessionIdGenerator: undefined,
|
|
62
|
+
enableJsonResponse: true
|
|
63
|
+
});
|
|
64
|
+
await mcpServer.connect(transport);
|
|
65
|
+
await transport.handleRequest(request, response);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
writeJson(response, 500, { error: message });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
await new Promise((resolve, reject) => {
|
|
73
|
+
server.once('error', reject);
|
|
74
|
+
server.listen(input.port, input.host, () => {
|
|
75
|
+
server.off('error', reject);
|
|
76
|
+
resolve();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
const address = server.address();
|
|
80
|
+
const port = typeof address === 'object' && address ? address.port : input.port;
|
|
81
|
+
const publicHost = input.host === '0.0.0.0' ? '127.0.0.1' : input.host;
|
|
82
|
+
const baseUrl = `http://${publicHost}:${port}`;
|
|
83
|
+
return {
|
|
84
|
+
url: `${baseUrl}${mcpPath}`,
|
|
85
|
+
healthUrl: `${baseUrl}/healthz`,
|
|
86
|
+
readyUrl: `${baseUrl}/readyz`,
|
|
87
|
+
close: () => new Promise((resolve, reject) => {
|
|
88
|
+
server.close((error) => {
|
|
89
|
+
if (error) {
|
|
90
|
+
reject(error);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
resolve();
|
|
94
|
+
});
|
|
95
|
+
})
|
|
96
|
+
};
|
|
97
|
+
};
|
package/docs/AGENT_USAGE.md
CHANGED
|
@@ -58,6 +58,20 @@ Guardrails for benchmark acceptance are configured with `searchPack.guardrailMin
|
|
|
58
58
|
`autoIndexOnWrite` (default: `true`) controls whether `add` and MCP write tools index right after writing.
|
|
59
59
|
`autoCanonicalContextLinks` (default: `true`) controls whether CLI/MCP write tools add canonical `## Context Links` to inferred context hubs.
|
|
60
60
|
|
|
61
|
+
## Central MCP Service
|
|
62
|
+
|
|
63
|
+
For clustered applications, run Brainlink as one central MCP service and let workloads connect to it over Streamable HTTP:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
BRAINLINK_MCP_TOKEN="change-me" brainlink mcp-server \
|
|
67
|
+
--vault /data/vault \
|
|
68
|
+
--host 0.0.0.0 \
|
|
69
|
+
--port 3333 \
|
|
70
|
+
--path /mcp
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The central service exposes `/mcp` for MCP clients plus `/healthz` and `/readyz` for platform probes. Use a Kubernetes `Service` for internal discovery, mount the Markdown vault through a persistent volume, and pass `Authorization: Bearer <token>` from clients when `BRAINLINK_MCP_TOKEN` or `--token` is configured.
|
|
74
|
+
|
|
61
75
|
## Agent Namespaces
|
|
62
76
|
|
|
63
77
|
Each agent writes into a dedicated namespace under `agents/<agent-id>/`:
|
|
@@ -371,6 +385,19 @@ blink config set-vault /absolute/path/to/vault --global
|
|
|
371
385
|
|
|
372
386
|
`config set-vault` updates Brainlink config through CLI. By default it writes local `brainlink.config.json`, appends the vault to `allowedVaults`, and migrates markdown when the target is empty.
|
|
373
387
|
|
|
388
|
+
### Manage Known Vaults
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
blink vaults list
|
|
392
|
+
blink vaults use /absolute/path/to/vault
|
|
393
|
+
blink vaults use /absolute/path/to/vault --global
|
|
394
|
+
blink vaults delete /absolute/path/to/vault --yes
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
`vaults list` shows the configured default vault, allowlisted vaults and the built-in default vault, including local existence and Markdown/index counts when available.
|
|
398
|
+
`vaults use` switches the default vault without migrating memory.
|
|
399
|
+
`vaults delete` deletes only local filesystem vaults, requires `--yes`, and refuses deleting the current default vault.
|
|
400
|
+
|
|
374
401
|
### Migrate Vaults Explicitly
|
|
375
402
|
|
|
376
403
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andespindola/brainlink",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.168",
|
|
4
4
|
"description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"build": "npm run clean && npx --yes snyk test && tsc -p tsconfig.json",
|
|
49
49
|
"dev": "tsx src/cli/main.ts",
|
|
50
50
|
"dev:mcp": "tsx src/mcp/main.ts",
|
|
51
|
+
"dev:mcp:http": "tsx src/cli/main.ts mcp-server",
|
|
51
52
|
"brainlink:sync": "bash scripts/brainlink-sync.sh",
|
|
52
53
|
"security": "npx --yes snyk test",
|
|
53
54
|
"test": "vitest run --config vitest.config.ts",
|