@andespindola/brainlink 0.1.0-beta.165 → 0.1.0-beta.167
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 +83 -0
- package/dist/application/analyze-vault.js +1 -1
- package/dist/application/build-context.js +5 -6
- package/dist/application/server/routes.js +7 -1
- package/dist/cli/commands/read-commands.js +1 -1
- package/dist/cli/commands/write-commands.js +34 -1
- package/dist/infrastructure/config.js +13 -2
- package/dist/mcp/http-server.js +97 -0
- package/dist/mcp/tools.js +3 -3
- package/docs/AGENT_USAGE.md +15 -1
- package/docs/QUICKSTART.md +2 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1049,6 +1049,89 @@ If no `vault` is configured and no `--vault` flag is passed, Brainlink uses `$HO
|
|
|
1049
1049
|
`autoIndexOnWrite` is optional and defaults to `true`. Set it to `false` to defer indexing after writes.
|
|
1050
1050
|
`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
1051
|
|
|
1052
|
+
## Remote MCP Server
|
|
1053
|
+
|
|
1054
|
+
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.
|
|
1055
|
+
|
|
1056
|
+
```bash
|
|
1057
|
+
BRAINLINK_MCP_TOKEN="change-me" brainlink mcp-server \
|
|
1058
|
+
--vault /data/vault \
|
|
1059
|
+
--host 0.0.0.0 \
|
|
1060
|
+
--port 3333 \
|
|
1061
|
+
--path /mcp
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
The server exposes:
|
|
1065
|
+
|
|
1066
|
+
```txt
|
|
1067
|
+
POST /mcp MCP Streamable HTTP endpoint
|
|
1068
|
+
GET /healthz liveness probe
|
|
1069
|
+
GET /readyz readiness probe
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
When `BRAINLINK_MCP_TOKEN` or `--token` is set, MCP requests must include:
|
|
1073
|
+
|
|
1074
|
+
```txt
|
|
1075
|
+
Authorization: Bearer <token>
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
For Kubernetes, run Brainlink as a central `Deployment` with a `Service` and a mounted vault volume:
|
|
1079
|
+
|
|
1080
|
+
```yaml
|
|
1081
|
+
apiVersion: apps/v1
|
|
1082
|
+
kind: Deployment
|
|
1083
|
+
metadata:
|
|
1084
|
+
name: brainlink-mcp
|
|
1085
|
+
spec:
|
|
1086
|
+
replicas: 1
|
|
1087
|
+
selector:
|
|
1088
|
+
matchLabels:
|
|
1089
|
+
app: brainlink-mcp
|
|
1090
|
+
template:
|
|
1091
|
+
metadata:
|
|
1092
|
+
labels:
|
|
1093
|
+
app: brainlink-mcp
|
|
1094
|
+
spec:
|
|
1095
|
+
containers:
|
|
1096
|
+
- name: brainlink
|
|
1097
|
+
image: brainlink:latest
|
|
1098
|
+
args: ["brainlink", "mcp-server", "--vault", "/data/vault", "--host", "0.0.0.0", "--port", "3333"]
|
|
1099
|
+
env:
|
|
1100
|
+
- name: BRAINLINK_MCP_TOKEN
|
|
1101
|
+
valueFrom:
|
|
1102
|
+
secretKeyRef:
|
|
1103
|
+
name: brainlink-mcp
|
|
1104
|
+
key: token
|
|
1105
|
+
ports:
|
|
1106
|
+
- containerPort: 3333
|
|
1107
|
+
readinessProbe:
|
|
1108
|
+
httpGet:
|
|
1109
|
+
path: /readyz
|
|
1110
|
+
port: 3333
|
|
1111
|
+
livenessProbe:
|
|
1112
|
+
httpGet:
|
|
1113
|
+
path: /healthz
|
|
1114
|
+
port: 3333
|
|
1115
|
+
volumeMounts:
|
|
1116
|
+
- name: vault
|
|
1117
|
+
mountPath: /data/vault
|
|
1118
|
+
volumes:
|
|
1119
|
+
- name: vault
|
|
1120
|
+
persistentVolumeClaim:
|
|
1121
|
+
claimName: brainlink-vault
|
|
1122
|
+
---
|
|
1123
|
+
apiVersion: v1
|
|
1124
|
+
kind: Service
|
|
1125
|
+
metadata:
|
|
1126
|
+
name: brainlink-mcp
|
|
1127
|
+
spec:
|
|
1128
|
+
selector:
|
|
1129
|
+
app: brainlink-mcp
|
|
1130
|
+
ports:
|
|
1131
|
+
- port: 3333
|
|
1132
|
+
targetPort: 3333
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1052
1135
|
Use `"embeddingProvider": "none"` when you want FTS-only indexing.
|
|
1053
1136
|
|
|
1054
1137
|
For local security checks, set your Snyk token in the environment:
|
|
@@ -50,7 +50,7 @@ export const getExtendedStats = async (vaultPath, agentId) => {
|
|
|
50
50
|
await searchKnowledge(absoluteVaultPath, probeQuery, Math.min(defaults.defaultSearchLimit, 8), agentId, 'hybrid');
|
|
51
51
|
const searchLatency = performance.now() - searchStart;
|
|
52
52
|
const contextStart = performance.now();
|
|
53
|
-
await buildContextPackage(absoluteVaultPath, probeQuery, Math.min(defaults.defaultSearchLimit, 8), defaults.defaultContextTokens, agentId, 'hybrid');
|
|
53
|
+
await buildContextPackage(absoluteVaultPath, probeQuery, Math.min(defaults.defaultSearchLimit, 8), defaults.defaultContextTokens, agentId, 'hybrid', undefined, defaults.defaultContextCacheTtlMs);
|
|
54
54
|
const contextLatency = performance.now() - contextStart;
|
|
55
55
|
return {
|
|
56
56
|
stats,
|
|
@@ -5,7 +5,6 @@ import { readContextPack, writeContextPack } from '../infrastructure/context-pac
|
|
|
5
5
|
import { indexStoragePath } from '../infrastructure/file-index.js';
|
|
6
6
|
import { searchVolatileMemory, volatileMemoryStoragePath } from '../infrastructure/volatile-memory.js';
|
|
7
7
|
import { searchKnowledge } from './search-knowledge.js';
|
|
8
|
-
const contextCacheTtlMs = 45_000;
|
|
9
8
|
const contextCacheMaxEntries = 200;
|
|
10
9
|
const contextCache = new Map();
|
|
11
10
|
const readFileSignature = async (path) => {
|
|
@@ -31,7 +30,7 @@ const withCacheMetadata = (context, cache) => ({
|
|
|
31
30
|
...context,
|
|
32
31
|
cache
|
|
33
32
|
});
|
|
34
|
-
const contextCacheGet = (key, dataSignature) => {
|
|
33
|
+
const contextCacheGet = (key, dataSignature, contextCacheTtlMs) => {
|
|
35
34
|
const entry = contextCache.get(key);
|
|
36
35
|
if (!entry) {
|
|
37
36
|
return undefined;
|
|
@@ -70,11 +69,11 @@ const emptyMetrics = (context, totalMs, overrides = {}) => ({
|
|
|
70
69
|
estimatedTokens: estimateSectionTokens(context),
|
|
71
70
|
...overrides
|
|
72
71
|
});
|
|
73
|
-
export const buildContextPackage = async (vaultPath, query, limit, maxTokens, agentId, mode, strategy = 'rag') => {
|
|
72
|
+
export const buildContextPackage = async (vaultPath, query, limit, maxTokens, agentId, mode, strategy = 'rag', contextCacheTtlMs = 120_000) => {
|
|
74
73
|
const totalStart = performance.now();
|
|
75
74
|
const cacheKey = toCacheKey(vaultPath, query, limit, maxTokens, agentId, mode, strategy);
|
|
76
75
|
const dataSignature = await readContextDataSignature(vaultPath);
|
|
77
|
-
const cached = contextCacheGet(cacheKey, dataSignature);
|
|
76
|
+
const cached = contextCacheGet(cacheKey, dataSignature, contextCacheTtlMs);
|
|
78
77
|
if (cached) {
|
|
79
78
|
return cached;
|
|
80
79
|
}
|
|
@@ -164,7 +163,7 @@ export const buildContextPackage = async (vaultPath, query, limit, maxTokens, ag
|
|
|
164
163
|
});
|
|
165
164
|
return contextWithMetrics;
|
|
166
165
|
};
|
|
167
|
-
export const buildContext = async (vaultPath, query, limit, maxTokens, agentId, mode, strategy) => {
|
|
168
|
-
const contextPackage = await buildContextPackage(vaultPath, query, limit, maxTokens, agentId, mode, strategy);
|
|
166
|
+
export const buildContext = async (vaultPath, query, limit, maxTokens, agentId, mode, strategy, contextCacheTtlMs = 120_000) => {
|
|
167
|
+
const contextPackage = await buildContextPackage(vaultPath, query, limit, maxTokens, agentId, mode, strategy, contextCacheTtlMs);
|
|
169
168
|
return contextPackage.content;
|
|
170
169
|
};
|
|
@@ -28,6 +28,11 @@ const readContextStrategy = async (url) => {
|
|
|
28
28
|
const defaults = resolveAgentRuntimeDefaults(config, readAgentQuery(url));
|
|
29
29
|
return sanitizeContextStrategy(url.searchParams.get('strategy'), defaults.defaultContextStrategy);
|
|
30
30
|
};
|
|
31
|
+
const readContextCacheTtlMs = async (url) => {
|
|
32
|
+
const config = await loadBrainlinkConfig();
|
|
33
|
+
const defaults = resolveAgentRuntimeDefaults(config, readAgentQuery(url));
|
|
34
|
+
return defaults.defaultContextCacheTtlMs;
|
|
35
|
+
};
|
|
31
36
|
const hasInvalidSearchMode = (url) => {
|
|
32
37
|
const mode = url.searchParams.get('mode');
|
|
33
38
|
return mode !== null && !['fts', 'semantic', 'hybrid'].includes(mode);
|
|
@@ -381,13 +386,14 @@ export const route = async (request, url, vaultPath) => {
|
|
|
381
386
|
const tokens = parsePositiveInteger(url.searchParams.get('tokens'), 2000);
|
|
382
387
|
const mode = await readSearchMode(url);
|
|
383
388
|
const strategy = await readContextStrategy(url);
|
|
389
|
+
const contextCacheTtlMs = await readContextCacheTtlMs(url);
|
|
384
390
|
if (hasInvalidSearchMode(url)) {
|
|
385
391
|
return createResponse(createJsonResponse({ error: 'Invalid mode. Use fts, semantic or hybrid.' }), 400, contentTypes['.json']);
|
|
386
392
|
}
|
|
387
393
|
if (hasInvalidContextStrategy(url)) {
|
|
388
394
|
return createResponse(createJsonResponse({ error: 'Invalid strategy. Use rag, cag or auto.' }), 400, contentTypes['.json']);
|
|
389
395
|
}
|
|
390
|
-
return createResponse(createJsonResponse(await buildContextPackage(vaultPath, query, limit, tokens, readAgentQuery(url), mode, strategy)), 200, contentTypes['.json']);
|
|
396
|
+
return createResponse(createJsonResponse(await buildContextPackage(vaultPath, query, limit, tokens, readAgentQuery(url), mode, strategy, contextCacheTtlMs)), 200, contentTypes['.json']);
|
|
391
397
|
}
|
|
392
398
|
if (isReadMethod(request) && url.pathname === '/api/links') {
|
|
393
399
|
return createResponse(createJsonResponse({ links: await listLinks(vaultPath, readAgentQuery(url)) }), 200, contentTypes['.json']);
|
|
@@ -69,7 +69,7 @@ export const registerReadCommands = (program) => {
|
|
|
69
69
|
const resolved = await resolveOptions(options);
|
|
70
70
|
const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
|
|
71
71
|
const strategy = sanitizeContextStrategy(options.strategy, resolved.defaults.defaultContextStrategy);
|
|
72
|
-
const contextPackage = await buildContextPackage(resolved.vault, query, parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit), parsePositiveInteger(options.tokens ?? String(resolved.defaults.defaultContextTokens), resolved.defaults.defaultContextTokens), resolved.agent, mode, strategy);
|
|
72
|
+
const contextPackage = await buildContextPackage(resolved.vault, query, parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit), parsePositiveInteger(options.tokens ?? String(resolved.defaults.defaultContextTokens), resolved.defaults.defaultContextTokens), resolved.agent, mode, strategy, resolved.defaults.defaultContextCacheTtlMs);
|
|
73
73
|
print(options.json, contextPackage, () => contextPackage.content);
|
|
74
74
|
});
|
|
75
75
|
program
|
|
@@ -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')
|
|
@@ -1135,7 +1168,7 @@ export const registerWriteCommands = (program) => {
|
|
|
1135
1168
|
const policy = await getBootstrapPolicy();
|
|
1136
1169
|
const bootstrapStatus = await getBootstrapSessionStatus(resolved.vault, resolved.agent);
|
|
1137
1170
|
const context = options.query
|
|
1138
|
-
? await buildContextPackage(resolved.vault, options.query, limit, tokens, resolved.agent, mode, strategy)
|
|
1171
|
+
? await buildContextPackage(resolved.vault, options.query, limit, tokens, resolved.agent, mode, strategy, resolved.defaults.defaultContextCacheTtlMs)
|
|
1139
1172
|
: null;
|
|
1140
1173
|
const agentIntegration = options.installAgent === false
|
|
1141
1174
|
? null
|
|
@@ -14,6 +14,7 @@ export const defaultBrainlinkConfig = {
|
|
|
14
14
|
defaultSearchLimit: 10,
|
|
15
15
|
defaultContextTokens: 2000,
|
|
16
16
|
defaultContextStrategy: 'rag',
|
|
17
|
+
defaultContextCacheTtlMs: 120_000,
|
|
17
18
|
embeddingProvider: 'local',
|
|
18
19
|
defaultSearchMode: 'hybrid',
|
|
19
20
|
chunkSize: 1200,
|
|
@@ -90,11 +91,17 @@ const sanitizeAgentProfile = (value) => {
|
|
|
90
91
|
const defaultContextStrategy = typeof value.defaultContextStrategy === 'string' && contextStrategies.has(value.defaultContextStrategy)
|
|
91
92
|
? value.defaultContextStrategy
|
|
92
93
|
: undefined;
|
|
94
|
+
const defaultContextCacheTtlMs = typeof value.defaultContextCacheTtlMs === 'number' &&
|
|
95
|
+
Number.isFinite(value.defaultContextCacheTtlMs) &&
|
|
96
|
+
value.defaultContextCacheTtlMs > 0
|
|
97
|
+
? Math.floor(value.defaultContextCacheTtlMs)
|
|
98
|
+
: undefined;
|
|
93
99
|
const profile = {
|
|
94
100
|
...(defaultSearchLimit ? { defaultSearchLimit } : {}),
|
|
95
101
|
...(defaultContextTokens ? { defaultContextTokens } : {}),
|
|
96
102
|
...(defaultSearchMode ? { defaultSearchMode } : {}),
|
|
97
|
-
...(defaultContextStrategy ? { defaultContextStrategy } : {})
|
|
103
|
+
...(defaultContextStrategy ? { defaultContextStrategy } : {}),
|
|
104
|
+
...(defaultContextCacheTtlMs ? { defaultContextCacheTtlMs } : {})
|
|
98
105
|
};
|
|
99
106
|
return Object.keys(profile).length > 0 ? profile : null;
|
|
100
107
|
};
|
|
@@ -177,6 +184,9 @@ const sanitizeConfig = (value) => ({
|
|
|
177
184
|
? value.defaultContextTokens
|
|
178
185
|
: defaultBrainlinkConfig.defaultContextTokens,
|
|
179
186
|
defaultContextStrategy: sanitizeContextStrategy(value.defaultContextStrategy, defaultBrainlinkConfig.defaultContextStrategy),
|
|
187
|
+
defaultContextCacheTtlMs: typeof value.defaultContextCacheTtlMs === 'number' && Number.isFinite(value.defaultContextCacheTtlMs) && value.defaultContextCacheTtlMs > 0
|
|
188
|
+
? Math.floor(value.defaultContextCacheTtlMs)
|
|
189
|
+
: defaultBrainlinkConfig.defaultContextCacheTtlMs,
|
|
180
190
|
allowedVaults: [...sanitizeAllowedVaults(value.allowedVaults), ...readAllowedVaultsFromEnv()],
|
|
181
191
|
chunkSize: typeof value.chunkSize === 'number' && value.chunkSize > 0 ? value.chunkSize : defaultBrainlinkConfig.chunkSize,
|
|
182
192
|
searchPack: sanitizeSearchPackConfig(value.searchPack),
|
|
@@ -191,7 +201,8 @@ export const resolveAgentRuntimeDefaults = (config, agent) => {
|
|
|
191
201
|
defaultSearchLimit: profile?.defaultSearchLimit ?? config.defaultSearchLimit,
|
|
192
202
|
defaultContextTokens: profile?.defaultContextTokens ?? config.defaultContextTokens,
|
|
193
203
|
defaultSearchMode: profile?.defaultSearchMode ?? config.defaultSearchMode,
|
|
194
|
-
defaultContextStrategy: profile?.defaultContextStrategy ?? config.defaultContextStrategy
|
|
204
|
+
defaultContextStrategy: profile?.defaultContextStrategy ?? config.defaultContextStrategy,
|
|
205
|
+
defaultContextCacheTtlMs: profile?.defaultContextCacheTtlMs ?? config.defaultContextCacheTtlMs
|
|
195
206
|
};
|
|
196
207
|
};
|
|
197
208
|
const mergeConfigLayers = (layers) => layers.reduce((state, config) => ({
|
|
@@ -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/dist/mcp/tools.js
CHANGED
|
@@ -399,7 +399,7 @@ export const contextTool = async (input) => {
|
|
|
399
399
|
const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
|
|
400
400
|
const limit = input.limit ?? context.defaults.defaultSearchLimit;
|
|
401
401
|
const tokens = input.tokens ?? context.defaults.defaultContextTokens;
|
|
402
|
-
const contextPackage = await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode, strategy);
|
|
402
|
+
const contextPackage = await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode, strategy, context.defaults.defaultContextCacheTtlMs);
|
|
403
403
|
const contextSession = await touchContextSession(context.vault, context.agent);
|
|
404
404
|
return jsonResult({
|
|
405
405
|
vault: context.vault,
|
|
@@ -718,7 +718,7 @@ export const syncTool = async (input) => {
|
|
|
718
718
|
const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
|
|
719
719
|
const contextLimit = input.contextLimit ?? context.defaults.defaultSearchLimit;
|
|
720
720
|
const contextTokens = input.contextTokens ?? context.defaults.defaultContextTokens;
|
|
721
|
-
const contextPackage = await buildContextPackage(context.vault, input.contextQuery, contextLimit, contextTokens, context.agent, mode, strategy);
|
|
721
|
+
const contextPackage = await buildContextPackage(context.vault, input.contextQuery, contextLimit, contextTokens, context.agent, mode, strategy, context.defaults.defaultContextCacheTtlMs);
|
|
722
722
|
const contextSession = await touchContextSession(context.vault, context.agent);
|
|
723
723
|
return jsonResult({
|
|
724
724
|
...response,
|
|
@@ -740,7 +740,7 @@ export const bootstrapTool = async (input) => {
|
|
|
740
740
|
const limit = input.limit ?? context.defaults.defaultSearchLimit;
|
|
741
741
|
const tokens = input.tokens ?? context.defaults.defaultContextTokens;
|
|
742
742
|
const contextPackage = input.query
|
|
743
|
-
? await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode, strategy)
|
|
743
|
+
? await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode, strategy, context.defaults.defaultContextCacheTtlMs)
|
|
744
744
|
: undefined;
|
|
745
745
|
const contextSession = input.query ? await touchContextSession(context.vault, context.agent) : undefined;
|
|
746
746
|
const guidance = stats.documentCount === 0
|
package/docs/AGENT_USAGE.md
CHANGED
|
@@ -51,13 +51,27 @@ Set `BRAINLINK_HOME` when the whole Brainlink home directory should live somewhe
|
|
|
51
51
|
Use `blink config where` and `blink config doctor` to inspect active paths and effective source.
|
|
52
52
|
|
|
53
53
|
You can also set `defaultAgent` in `brainlink.config.json` / `.brainlink.json` (for example `"defaultAgent": "coding-agent"`). When set, CLI commands and MCP calls reuse it when `--agent`/`agent` is not passed.
|
|
54
|
-
You can set `agentProfiles` to define per-agent defaults for `defaultSearchMode`, `defaultSearchLimit`, `defaultContextTokens` and `
|
|
54
|
+
You can set `agentProfiles` to define per-agent defaults for `defaultSearchMode`, `defaultSearchLimit`, `defaultContextTokens`, `defaultContextStrategy` and `defaultContextCacheTtlMs`.
|
|
55
55
|
You can tune search-pack compression with `searchPack.rowChunkSize`, `searchPack.compressionLevel` and `searchPack.useDictionary`.
|
|
56
56
|
Guardrails for benchmark acceptance are configured with `searchPack.guardrailMinSavingsPercent` and `searchPack.guardrailMaxLatencyRegressionPercent`.
|
|
57
57
|
|
|
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>/`:
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -51,7 +51,8 @@ Optional per-agent retrieval defaults in `brainlink.config.json`:
|
|
|
51
51
|
"defaultSearchMode": "semantic",
|
|
52
52
|
"defaultSearchLimit": 8,
|
|
53
53
|
"defaultContextTokens": 2400,
|
|
54
|
-
"defaultContextStrategy": "auto"
|
|
54
|
+
"defaultContextStrategy": "auto",
|
|
55
|
+
"defaultContextCacheTtlMs": 120000
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
}
|
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.167",
|
|
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",
|