@icoretech/warden-mcp 0.1.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 +21 -0
- package/README.md +156 -0
- package/bin/warden-mcp.js +60 -0
- package/dist/app.js +3 -0
- package/dist/bw/bwCli.js +106 -0
- package/dist/bw/bwHeaders.js +87 -0
- package/dist/bw/bwPool.js +54 -0
- package/dist/bw/bwSession.js +230 -0
- package/dist/bw/mutex.js +19 -0
- package/dist/sdk/generateArgs.js +64 -0
- package/dist/sdk/keychainSdk.js +1225 -0
- package/dist/sdk/patch.js +34 -0
- package/dist/sdk/redact.js +76 -0
- package/dist/sdk/types.js +2 -0
- package/dist/sdk/usernameGenerator.js +142 -0
- package/dist/server.js +22 -0
- package/dist/tools/registerTools.js +1250 -0
- package/dist/transports/http.js +376 -0
- package/dist/transports/stdio.js +33 -0
- package/package.json +52 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// src/app.ts
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
5
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import { bwEnvFromHeadersOrEnv } from '../bw/bwHeaders.js';
|
|
8
|
+
import { BwSessionPool } from '../bw/bwPool.js';
|
|
9
|
+
import { KeychainSdk } from '../sdk/keychainSdk.js';
|
|
10
|
+
import { registerTools } from '../tools/registerTools.js';
|
|
11
|
+
export function createKeychainApp(opts = {}) {
|
|
12
|
+
const parsePositiveInt = (raw, fallback) => {
|
|
13
|
+
const parsed = Number.parseInt(raw ?? '', 10);
|
|
14
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
15
|
+
};
|
|
16
|
+
const parseNonNegativeInt = (raw, fallback) => {
|
|
17
|
+
const parsed = Number.parseInt(raw ?? '', 10);
|
|
18
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
19
|
+
};
|
|
20
|
+
const TOOL_PREFIX = opts.toolPrefix ?? process.env.TOOL_PREFIX ?? 'keychain';
|
|
21
|
+
const APP_NAME = opts.appName ?? process.env.MCP_APP_NAME ?? `${TOOL_PREFIX}-mcp`;
|
|
22
|
+
const sessionTtlMs = opts.sessionTtlMs ??
|
|
23
|
+
parsePositiveInt(process.env.KEYCHAIN_SESSION_TTL_MS, 15 * 60 * 1000);
|
|
24
|
+
const sessionMaxCount = opts.sessionMaxCount ??
|
|
25
|
+
parsePositiveInt(process.env.KEYCHAIN_SESSION_MAX_COUNT, 32);
|
|
26
|
+
const sessionSweepIntervalMs = Math.max(1_000, opts.sessionSweepIntervalMs ??
|
|
27
|
+
parsePositiveInt(process.env.KEYCHAIN_SESSION_SWEEP_INTERVAL_MS, 60_000));
|
|
28
|
+
const maxHeapUsedBytesFuse = opts.maxHeapUsedBytesFuse ??
|
|
29
|
+
(() => {
|
|
30
|
+
const maxHeapUsedMb = parseNonNegativeInt(process.env.KEYCHAIN_MAX_HEAP_USED_MB, 1536);
|
|
31
|
+
if (maxHeapUsedMb === 0)
|
|
32
|
+
return Number.POSITIVE_INFINITY;
|
|
33
|
+
return maxHeapUsedMb * 1024 * 1024;
|
|
34
|
+
})();
|
|
35
|
+
const metricsLogIntervalMs = opts.metricsLogIntervalMs ??
|
|
36
|
+
parseNonNegativeInt(process.env.KEYCHAIN_METRICS_LOG_INTERVAL_MS, 0);
|
|
37
|
+
const pool = new BwSessionPool({
|
|
38
|
+
rootDir: opts.bwHomeRoot ??
|
|
39
|
+
process.env.KEYCHAIN_BW_HOME_ROOT ??
|
|
40
|
+
`${process.env.HOME ?? '/data'}/bw-profiles`,
|
|
41
|
+
});
|
|
42
|
+
function createMcpServer() {
|
|
43
|
+
const server = new McpServer({ name: APP_NAME, version: '0.1.0' });
|
|
44
|
+
registerTools(server, {
|
|
45
|
+
getSdk: async (authInfo) => {
|
|
46
|
+
const extra = authInfo?.extra;
|
|
47
|
+
const bwEnvFromHeader = extra?.bwEnv;
|
|
48
|
+
if (bwEnvFromHeader && typeof bwEnvFromHeader === 'object') {
|
|
49
|
+
const bw = await pool.getOrCreate(bwEnvFromHeader);
|
|
50
|
+
return new KeychainSdk(bw);
|
|
51
|
+
}
|
|
52
|
+
throw new Error('Missing Bitwarden config headers. Provide X-BW-Host, X-BW-Password, and either (X-BW-ClientId + X-BW-ClientSecret) or X-BW-User.');
|
|
53
|
+
},
|
|
54
|
+
toolPrefix: TOOL_PREFIX,
|
|
55
|
+
});
|
|
56
|
+
return server;
|
|
57
|
+
}
|
|
58
|
+
async function withBwHeaders(req) {
|
|
59
|
+
const bwEnv = bwEnvFromHeadersOrEnv(req);
|
|
60
|
+
req.auth = bwEnv
|
|
61
|
+
? {
|
|
62
|
+
token: 'x-bw-headers',
|
|
63
|
+
clientId: 'x-bw-headers',
|
|
64
|
+
scopes: [],
|
|
65
|
+
extra: { bwEnv },
|
|
66
|
+
}
|
|
67
|
+
: undefined;
|
|
68
|
+
}
|
|
69
|
+
const app = express();
|
|
70
|
+
app.use(express.json({ limit: '4mb' }));
|
|
71
|
+
app.get('/healthz', (_req, res) => res.status(200).send('ok'));
|
|
72
|
+
// Keep per-session transports and servers (Streamable HTTP stateful mode).
|
|
73
|
+
const sessions = new Map();
|
|
74
|
+
const counters = {
|
|
75
|
+
rejected_sessions_429: 0,
|
|
76
|
+
rejected_sessions_503: 0,
|
|
77
|
+
session_ttl_evictions: 0,
|
|
78
|
+
sessions_created: 0,
|
|
79
|
+
sessions_closed: 0,
|
|
80
|
+
};
|
|
81
|
+
const closeSession = (sid, reason = 'close') => {
|
|
82
|
+
const existing = sessions.get(sid);
|
|
83
|
+
if (!existing)
|
|
84
|
+
return;
|
|
85
|
+
sessions.delete(sid);
|
|
86
|
+
counters.sessions_closed += 1;
|
|
87
|
+
if (reason === 'ttl')
|
|
88
|
+
counters.session_ttl_evictions += 1;
|
|
89
|
+
existing.transport.onclose = undefined;
|
|
90
|
+
queueMicrotask(() => {
|
|
91
|
+
existing.server.close();
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const sweepSessions = () => {
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
for (const [sid, entry] of sessions.entries()) {
|
|
97
|
+
if (now - entry.lastSeenAt > sessionTtlMs) {
|
|
98
|
+
closeSession(sid, 'ttl');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const trackSession = (sid, transport, server) => {
|
|
103
|
+
closeSession(sid, 'replacement');
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
sessions.set(sid, { transport, server, createdAt: now, lastSeenAt: now });
|
|
106
|
+
counters.sessions_created += 1;
|
|
107
|
+
let closing = false;
|
|
108
|
+
transport.onclose = () => {
|
|
109
|
+
if (closing)
|
|
110
|
+
return;
|
|
111
|
+
closing = true;
|
|
112
|
+
const current = sessions.get(sid);
|
|
113
|
+
if (current && current.transport === transport) {
|
|
114
|
+
sessions.delete(sid);
|
|
115
|
+
counters.sessions_closed += 1;
|
|
116
|
+
}
|
|
117
|
+
queueMicrotask(() => {
|
|
118
|
+
transport.onclose = undefined;
|
|
119
|
+
server.close();
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
sweepSessions();
|
|
123
|
+
};
|
|
124
|
+
const touchSession = (sid) => {
|
|
125
|
+
const existing = sessions.get(sid);
|
|
126
|
+
if (!existing)
|
|
127
|
+
return;
|
|
128
|
+
existing.lastSeenAt = Date.now();
|
|
129
|
+
};
|
|
130
|
+
const collectMetrics = () => {
|
|
131
|
+
const memoryUsage = process.memoryUsage();
|
|
132
|
+
const fuseTripped = memoryUsage.heapUsed >= maxHeapUsedBytesFuse;
|
|
133
|
+
return {
|
|
134
|
+
session: {
|
|
135
|
+
active_sessions: sessions.size,
|
|
136
|
+
max_sessions: sessionMaxCount,
|
|
137
|
+
ttl_ms: sessionTtlMs,
|
|
138
|
+
},
|
|
139
|
+
counters: { ...counters },
|
|
140
|
+
memory: {
|
|
141
|
+
heap_used_bytes: memoryUsage.heapUsed,
|
|
142
|
+
rss_bytes: memoryUsage.rss,
|
|
143
|
+
max_heap_used_bytes_fuse: Number.isFinite(maxHeapUsedBytesFuse)
|
|
144
|
+
? maxHeapUsedBytesFuse
|
|
145
|
+
: -1,
|
|
146
|
+
fuse_tripped: fuseTripped,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
const rejectIfSessionCapacityReached = (res) => {
|
|
151
|
+
if (sessions.size < sessionMaxCount)
|
|
152
|
+
return false;
|
|
153
|
+
counters.rejected_sessions_429 += 1;
|
|
154
|
+
res
|
|
155
|
+
.status(429)
|
|
156
|
+
.setHeader('retry-after', '1')
|
|
157
|
+
.json({
|
|
158
|
+
jsonrpc: '2.0',
|
|
159
|
+
error: {
|
|
160
|
+
code: -32000,
|
|
161
|
+
message: 'Too many active MCP sessions. Reuse an existing mcp-session-id or retry shortly.',
|
|
162
|
+
},
|
|
163
|
+
id: null,
|
|
164
|
+
});
|
|
165
|
+
return true;
|
|
166
|
+
};
|
|
167
|
+
const rejectIfMemoryFuseTripped = (res) => {
|
|
168
|
+
if (!Number.isFinite(maxHeapUsedBytesFuse))
|
|
169
|
+
return false;
|
|
170
|
+
const heapUsed = process.memoryUsage().heapUsed;
|
|
171
|
+
if (heapUsed < maxHeapUsedBytesFuse)
|
|
172
|
+
return false;
|
|
173
|
+
counters.rejected_sessions_503 += 1;
|
|
174
|
+
res
|
|
175
|
+
.status(503)
|
|
176
|
+
.setHeader('retry-after', '2')
|
|
177
|
+
.json({
|
|
178
|
+
jsonrpc: '2.0',
|
|
179
|
+
error: {
|
|
180
|
+
code: -32000,
|
|
181
|
+
message: 'Server under memory pressure. Reuse an existing mcp-session-id or retry shortly.',
|
|
182
|
+
},
|
|
183
|
+
id: null,
|
|
184
|
+
});
|
|
185
|
+
return true;
|
|
186
|
+
};
|
|
187
|
+
const sweepTimer = setInterval(() => {
|
|
188
|
+
sweepSessions();
|
|
189
|
+
}, sessionSweepIntervalMs);
|
|
190
|
+
sweepTimer.unref?.();
|
|
191
|
+
if (metricsLogIntervalMs > 0) {
|
|
192
|
+
const metricsTimer = setInterval(() => {
|
|
193
|
+
const metrics = collectMetrics();
|
|
194
|
+
console.log(`[metrics] sessions=${metrics.session.active_sessions}/${metrics.session.max_sessions} heap_used=${metrics.memory.heap_used_bytes} rejected429=${metrics.counters.rejected_sessions_429} rejected503=${metrics.counters.rejected_sessions_503} ttl_evictions=${metrics.counters.session_ttl_evictions}`);
|
|
195
|
+
}, metricsLogIntervalMs);
|
|
196
|
+
metricsTimer.unref?.();
|
|
197
|
+
}
|
|
198
|
+
app.get('/metricsz', (_req, res) => {
|
|
199
|
+
res.status(200).json(collectMetrics());
|
|
200
|
+
});
|
|
201
|
+
app.all('/sse', async (req, res) => {
|
|
202
|
+
sweepSessions();
|
|
203
|
+
const debugHttp = (process.env.KEYCHAIN_DEBUG_HTTP ?? 'false').toLowerCase() === 'true';
|
|
204
|
+
if (debugHttp) {
|
|
205
|
+
const accept = req.header('accept');
|
|
206
|
+
const ct = req.header('content-type');
|
|
207
|
+
const sid = req.header('mcp-session-id');
|
|
208
|
+
const proto = req.header('mcp-protocol-version');
|
|
209
|
+
const hasBw = Boolean(req.header('x-bw-host')) ||
|
|
210
|
+
Boolean(req.header('x-bw-user')) ||
|
|
211
|
+
Boolean(req.header('x-bw-clientid'));
|
|
212
|
+
const body = req.body;
|
|
213
|
+
const bodyMethod = body && typeof body === 'object'
|
|
214
|
+
? String(body.method ?? '-')
|
|
215
|
+
: '-';
|
|
216
|
+
console.log(`[http] ${req.method} ${req.path} m=${bodyMethod} accept=${accept ?? '-'} ct=${ct ?? '-'} sid=${sid ? 'yes' : 'no'} proto=${proto ?? '-'} bw=${hasBw ? 'yes' : 'no'}`);
|
|
217
|
+
res.once('finish', () => {
|
|
218
|
+
const rct = res.getHeader('content-type');
|
|
219
|
+
console.log(`[http] -> ${res.statusCode} ct=${typeof rct === 'string' ? rct : '-'}`);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
// Some MCP clients probe DELETE support and may behave poorly if the server
|
|
223
|
+
// actually terminates sessions. We intentionally do not support DELETE here.
|
|
224
|
+
if (req.method === 'DELETE') {
|
|
225
|
+
res
|
|
226
|
+
.status(405)
|
|
227
|
+
.setHeader('allow', 'GET, POST')
|
|
228
|
+
.json({
|
|
229
|
+
jsonrpc: '2.0',
|
|
230
|
+
error: { code: -32000, message: 'Method not allowed.' },
|
|
231
|
+
id: null,
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
await withBwHeaders(req);
|
|
236
|
+
const sessionIdHeader = req.headers['mcp-session-id'];
|
|
237
|
+
const sessionId = typeof sessionIdHeader === 'string'
|
|
238
|
+
? sessionIdHeader
|
|
239
|
+
: Array.isArray(sessionIdHeader)
|
|
240
|
+
? sessionIdHeader[0]
|
|
241
|
+
: undefined;
|
|
242
|
+
try {
|
|
243
|
+
const body = req.body;
|
|
244
|
+
const isInit = req.method === 'POST' && isInitializeRequest(body);
|
|
245
|
+
// Reuse an existing session, or recover it if the client is holding a stale ID.
|
|
246
|
+
if (sessionId) {
|
|
247
|
+
const existing = sessions.get(sessionId);
|
|
248
|
+
if (existing) {
|
|
249
|
+
touchSession(sessionId);
|
|
250
|
+
await existing.transport.handleRequest(req, res, body);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
// If we got an initialize request with a client-supplied session id,
|
|
254
|
+
// accept it (Codex tends to cache session ids across restarts).
|
|
255
|
+
if (isInit) {
|
|
256
|
+
if (rejectIfMemoryFuseTripped(res))
|
|
257
|
+
return;
|
|
258
|
+
if (rejectIfSessionCapacityReached(res))
|
|
259
|
+
return;
|
|
260
|
+
const server = createMcpServer();
|
|
261
|
+
let transport;
|
|
262
|
+
let sidForCleanup;
|
|
263
|
+
transport = new StreamableHTTPServerTransport({
|
|
264
|
+
sessionIdGenerator: () => sessionId,
|
|
265
|
+
enableJsonResponse: true,
|
|
266
|
+
onsessioninitialized: (sid) => {
|
|
267
|
+
sidForCleanup = sid;
|
|
268
|
+
trackSession(sid, transport, server);
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
try {
|
|
272
|
+
await server.connect(transport);
|
|
273
|
+
await transport.handleRequest(req, res, body);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
if (sidForCleanup) {
|
|
277
|
+
closeSession(sidForCleanup);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
transport.onclose = undefined;
|
|
281
|
+
queueMicrotask(() => {
|
|
282
|
+
server.close();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// For non-initialize requests with an unknown session id, create a new
|
|
290
|
+
// session and treat it as already initialized (best-effort compatibility).
|
|
291
|
+
if (rejectIfMemoryFuseTripped(res))
|
|
292
|
+
return;
|
|
293
|
+
if (rejectIfSessionCapacityReached(res))
|
|
294
|
+
return;
|
|
295
|
+
const server = createMcpServer();
|
|
296
|
+
let transport;
|
|
297
|
+
transport = new StreamableHTTPServerTransport({
|
|
298
|
+
sessionIdGenerator: () => sessionId,
|
|
299
|
+
enableJsonResponse: true,
|
|
300
|
+
});
|
|
301
|
+
const wtUnknown = transport._webStandardTransport;
|
|
302
|
+
if (wtUnknown && typeof wtUnknown === 'object') {
|
|
303
|
+
const wt = wtUnknown;
|
|
304
|
+
wt.sessionId = sessionId;
|
|
305
|
+
wt._initialized = true;
|
|
306
|
+
}
|
|
307
|
+
trackSession(sessionId, transport, server);
|
|
308
|
+
try {
|
|
309
|
+
await server.connect(transport);
|
|
310
|
+
await transport.handleRequest(req, res, body);
|
|
311
|
+
touchSession(sessionId);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
closeSession(sessionId, 'error');
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// Initialize a new session
|
|
320
|
+
if (!sessionId &&
|
|
321
|
+
req.method === 'POST' &&
|
|
322
|
+
isInitializeRequest(req.body)) {
|
|
323
|
+
if (rejectIfMemoryFuseTripped(res))
|
|
324
|
+
return;
|
|
325
|
+
if (rejectIfSessionCapacityReached(res))
|
|
326
|
+
return;
|
|
327
|
+
const server = createMcpServer();
|
|
328
|
+
let transport;
|
|
329
|
+
let sidForCleanup;
|
|
330
|
+
transport = new StreamableHTTPServerTransport({
|
|
331
|
+
sessionIdGenerator: () => randomUUID(),
|
|
332
|
+
enableJsonResponse: true,
|
|
333
|
+
onsessioninitialized: (sid) => {
|
|
334
|
+
sidForCleanup = sid;
|
|
335
|
+
trackSession(sid, transport, server);
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
try {
|
|
339
|
+
await server.connect(transport);
|
|
340
|
+
await transport.handleRequest(req, res, body);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (sidForCleanup) {
|
|
344
|
+
closeSession(sidForCleanup, 'error');
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
transport.onclose = undefined;
|
|
348
|
+
queueMicrotask(() => {
|
|
349
|
+
server.close();
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
res.status(400).json({
|
|
357
|
+
jsonrpc: '2.0',
|
|
358
|
+
error: {
|
|
359
|
+
code: -32000,
|
|
360
|
+
message: 'Bad Request: No valid session ID provided',
|
|
361
|
+
},
|
|
362
|
+
id: null,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
if (!res.headersSent) {
|
|
367
|
+
res.status(500).json({
|
|
368
|
+
jsonrpc: '2.0',
|
|
369
|
+
error: { code: -32603, message: 'Internal server error' },
|
|
370
|
+
id: null,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
return app;
|
|
376
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/transports/stdio.ts
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { BwSessionPool } from '../bw/bwPool.js';
|
|
5
|
+
import { readBwEnv } from '../bw/bwSession.js';
|
|
6
|
+
import { KeychainSdk } from '../sdk/keychainSdk.js';
|
|
7
|
+
import { registerTools } from '../tools/registerTools.js';
|
|
8
|
+
export async function runStdioTransport() {
|
|
9
|
+
const TOOL_PREFIX = process.env.TOOL_PREFIX ?? 'keychain';
|
|
10
|
+
const APP_NAME = process.env.MCP_APP_NAME ?? `${TOOL_PREFIX}-mcp`;
|
|
11
|
+
// Credentials must be present at startup for stdio mode.
|
|
12
|
+
const bwEnv = readBwEnv();
|
|
13
|
+
const pool = new BwSessionPool({
|
|
14
|
+
rootDir: process.env.KEYCHAIN_BW_HOME_ROOT ??
|
|
15
|
+
`${process.env.HOME ?? '/data'}/bw-profiles`,
|
|
16
|
+
});
|
|
17
|
+
const server = new McpServer({ name: APP_NAME, version: '0.1.0' });
|
|
18
|
+
registerTools(server, {
|
|
19
|
+
getSdk: async () => {
|
|
20
|
+
const bw = await pool.getOrCreate(bwEnv);
|
|
21
|
+
return new KeychainSdk(bw);
|
|
22
|
+
},
|
|
23
|
+
toolPrefix: TOOL_PREFIX,
|
|
24
|
+
});
|
|
25
|
+
const transport = new StdioServerTransport();
|
|
26
|
+
// Assign onclose BEFORE connect() to avoid a race where stdin is already
|
|
27
|
+
// EOF when the process starts (connect() calls transport.start() internally).
|
|
28
|
+
const closed = new Promise((resolve) => {
|
|
29
|
+
transport.onclose = resolve;
|
|
30
|
+
});
|
|
31
|
+
await server.connect(transport);
|
|
32
|
+
await closed;
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"private": false,
|
|
3
|
+
"name": "@icoretech/warden-mcp",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Vaultwarden/Bitwarden MCP server backed by Bitwarden CLI (bw).",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/icoretech/warden-mcp.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/icoretech/warden-mcp#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/icoretech/warden-mcp/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"warden-mcp": "bin/warden-mcp.js"
|
|
17
|
+
},
|
|
18
|
+
"files": ["dist/", "bin/", "!dist/**/*.test.js", "!dist/integration/"],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"dev": "tsx watch --clear-screen=false src/server.ts",
|
|
21
|
+
"build": "tsc -p .",
|
|
22
|
+
"start": "node dist/server.js",
|
|
23
|
+
"test": "npm run build && node --test \"dist/**/*.test.js\"",
|
|
24
|
+
"test:integration": "npm run build && node --test --test-timeout=45000 \"dist/integration/**/*.test.js\"",
|
|
25
|
+
"test:session-regression": "node scripts/session-flood-regression.mjs",
|
|
26
|
+
"lint": "biome check --write --assist-enabled=true . && tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
30
|
+
"express": "^5.2.1",
|
|
31
|
+
"jose": "^6.1.3",
|
|
32
|
+
"zod": "^4.3.6"
|
|
33
|
+
},
|
|
34
|
+
"optionalDependencies": {
|
|
35
|
+
"@bitwarden/cli": "2026.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@biomejs/biome": "^2.3.15",
|
|
39
|
+
"@types/express": "^5.0.6",
|
|
40
|
+
"@types/node": "^25.2.3",
|
|
41
|
+
"playwright": "1.58.2",
|
|
42
|
+
"tsx": "^4.21.0",
|
|
43
|
+
"typescript": "^5.9.3"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"registry": "https://registry.npmjs.org",
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=24.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|