kojee-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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kojee AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # kojee-mcp
2
+
3
+ kojee-mcp is a local MCP proxy that lets any MCP-capable agent use Kojee-managed tools seamlessly. It handles DPoP authentication, key enrollment, nonce rotation, step-up re-auth, and governance flows internally -- the agent just sees tools and calls them.
4
+
5
+ ## Quick Start
6
+
7
+ 1. **Get a gateway token** from the [Kojee dashboard](https://kojee.ai).
8
+ 2. **Add to your MCP config** (see examples below).
9
+ 3. **Tools appear automatically** -- the proxy discovers and exposes all tools your token has access to.
10
+
11
+ ## MCP Config Examples
12
+
13
+ ### Claude Code / Claude Desktop
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "kojee": {
19
+ "command": "npx",
20
+ "args": ["kojee-mcp", "--token", "YOUR_TOKEN", "--url", "https://kojee.ai"]
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ ### Generic MCP Client
27
+
28
+ Any MCP client that supports stdio transports can use kojee-mcp. Point it at the CLI binary:
29
+
30
+ ```bash
31
+ npx kojee-mcp --token gw_abc123... --url https://kojee.ai
32
+ ```
33
+
34
+ Or install globally:
35
+
36
+ ```bash
37
+ npm install -g kojee-mcp
38
+ kojee-mcp --token gw_abc123... --url https://kojee.ai
39
+ ```
40
+
41
+ ## CLI Flags
42
+
43
+ | Flag | Required | Default | Description |
44
+ |------|----------|---------|-------------|
45
+ | `--token` | yes | -- | Your Kojee gateway token (starts with `gw_`) |
46
+ | `--url` | yes | -- | Kojee broker URL (e.g. `https://kojee.ai`) |
47
+ | `--keystore-path` | no | `~/.kojee/keypair.json` | Path for DPoP keypair storage |
48
+
49
+ ## How Approvals Work
50
+
51
+ Some tools are governed by approval policies configured in the Kojee dashboard. When an agent calls a governed tool:
52
+
53
+ 1. The proxy submits the call to the Kojee broker.
54
+ 2. If the policy requires approval, the broker returns a `pending_approval` status.
55
+ 3. The proxy translates this into a clear MCP response telling the agent that the action is awaiting human approval.
56
+ 4. Once approved (via dashboard, Slack, or other configured channel), the agent can retry the call and it will succeed.
57
+
58
+ Step-up re-authentication and nonce rotation are handled transparently -- the agent never needs to deal with auth mechanics.
59
+
60
+ ## Troubleshooting
61
+
62
+ **"Gateway token does not start with gw_"**
63
+ You may be using an API key instead of a gateway token. Generate a gateway token from the Kojee dashboard under Settings > Gateway Tokens.
64
+
65
+ **"Fatal error: Failed to enroll keypair"**
66
+ The proxy cannot reach the broker URL. Check that `--url` is correct and your network allows outbound HTTPS.
67
+
68
+ **No tools appear**
69
+ Your gateway token may not have any connectors assigned. Check the Kojee dashboard to ensure at least one connector is linked to the token.
70
+
71
+ **Keypair permission errors**
72
+ The proxy stores its DPoP keypair at `~/.kojee/keypair.json` by default. Ensure the directory is writable, or use `--keystore-path` to specify an alternative location.
73
+
74
+ ## License
75
+
76
+ MIT
@@ -0,0 +1,729 @@
1
+ // src/index.ts
2
+ import path2 from "path";
3
+
4
+ // src/auth/auth-module.ts
5
+ import { calculateJwkThumbprint } from "jose";
6
+ import crypto from "crypto";
7
+
8
+ // src/auth/keystore.ts
9
+ import { importJWK, exportJWK, generateKeyPair } from "jose";
10
+ import fs from "fs";
11
+ import path from "path";
12
+ var DEFAULT_PATH = path.join(
13
+ process.env["HOME"] ?? "~",
14
+ ".kojee",
15
+ "keypair.json"
16
+ );
17
+ async function loadKeystore(keystorePath = DEFAULT_PATH, expectedBrokerUrl) {
18
+ if (!fs.existsSync(keystorePath)) {
19
+ return null;
20
+ }
21
+ const raw = fs.readFileSync(keystorePath, "utf-8");
22
+ const data = JSON.parse(raw);
23
+ if (expectedBrokerUrl && data.broker_url !== expectedBrokerUrl) {
24
+ return null;
25
+ }
26
+ const privateKey = await importJWK(data.private_key_jwk, "ES256");
27
+ return {
28
+ privateKey,
29
+ publicJwk: data.public_jwk,
30
+ kid: data.kid,
31
+ data
32
+ };
33
+ }
34
+ async function saveKeystore(privateKey, publicJwk, kid, brokerUrl, keystorePath = DEFAULT_PATH) {
35
+ const dir = path.dirname(keystorePath);
36
+ if (!fs.existsSync(dir)) {
37
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
38
+ }
39
+ const privateJwk = await exportJWK(privateKey);
40
+ const data = {
41
+ private_key_jwk: privateJwk,
42
+ kid,
43
+ broker_url: brokerUrl,
44
+ public_jwk: publicJwk,
45
+ enrolled_at: (/* @__PURE__ */ new Date()).toISOString()
46
+ };
47
+ fs.writeFileSync(keystorePath, JSON.stringify(data, null, 2), {
48
+ mode: 384
49
+ });
50
+ }
51
+ async function generateES256KeyPair() {
52
+ const { privateKey, publicKey } = await generateKeyPair("ES256");
53
+ const publicJwk = await exportJWK(publicKey);
54
+ publicJwk.kty = "EC";
55
+ publicJwk.crv = "P-256";
56
+ return { privateKey, publicJwk };
57
+ }
58
+
59
+ // src/auth/registration.ts
60
+ async function registerKey(brokerUrl, token, publicJwk) {
61
+ const url = `${brokerUrl}/api/v1/bots/keys/register/`;
62
+ const response = await fetch(url, {
63
+ method: "POST",
64
+ headers: {
65
+ "Content-Type": "application/json",
66
+ Authorization: `Bearer ${token}`
67
+ },
68
+ body: JSON.stringify({ public_jwk: publicJwk })
69
+ });
70
+ if (!response.ok) {
71
+ const body = await response.text();
72
+ throw new Error(
73
+ `Key registration failed (${response.status}): ${body}`
74
+ );
75
+ }
76
+ return await response.json();
77
+ }
78
+ async function confirmKey(brokerUrl, token, botKeyId, challenge, signature) {
79
+ const url = `${brokerUrl}/api/v1/bots/keys/confirm/`;
80
+ const response = await fetch(url, {
81
+ method: "POST",
82
+ headers: {
83
+ "Content-Type": "application/json",
84
+ Authorization: `Bearer ${token}`
85
+ },
86
+ body: JSON.stringify({
87
+ bot_key_id: botKeyId,
88
+ challenge,
89
+ signature
90
+ })
91
+ });
92
+ if (!response.ok) {
93
+ const body = await response.text();
94
+ throw new Error(
95
+ `Key confirmation failed (${response.status}): ${body}`
96
+ );
97
+ }
98
+ return await response.json();
99
+ }
100
+
101
+ // src/auth/auth-module.ts
102
+ async function signChallengeRaw(privateKey, data) {
103
+ const signer = crypto.createSign("SHA256");
104
+ signer.update(data);
105
+ signer.end();
106
+ const derSignature = signer.sign(
107
+ privateKey
108
+ );
109
+ return derSignature.toString("base64url");
110
+ }
111
+ var AuthModule = class {
112
+ constructor(token, brokerUrl, keystorePath) {
113
+ this.token = token;
114
+ this.brokerUrl = brokerUrl;
115
+ this.keystorePath = keystorePath;
116
+ }
117
+ token;
118
+ brokerUrl;
119
+ keystorePath;
120
+ privateKey = null;
121
+ publicJwk = null;
122
+ kid = null;
123
+ /**
124
+ * Ensure we have an enrolled keypair. Either loads from disk or
125
+ * performs the full enrollment flow.
126
+ */
127
+ async ensureEnrolled() {
128
+ const existing = await loadKeystore(this.keystorePath, this.brokerUrl);
129
+ if (existing) {
130
+ this.privateKey = existing.privateKey;
131
+ this.publicJwk = existing.publicJwk;
132
+ this.kid = existing.kid;
133
+ console.error("[auth] Loaded existing keypair from keystore");
134
+ return {
135
+ privateKey: existing.privateKey,
136
+ publicJwk: existing.publicJwk,
137
+ kid: existing.kid
138
+ };
139
+ }
140
+ console.error("[auth] No valid keystore found, enrolling new keypair...");
141
+ const { privateKey, publicJwk } = await generateES256KeyPair();
142
+ const regResult = await registerKey(this.brokerUrl, this.token, publicJwk);
143
+ console.error(`[auth] Key registered: ${regResult.bot_key_id}`);
144
+ const thumbprint = await calculateJwkThumbprint(publicJwk, "sha256");
145
+ const challengeData = `${regResult.challenge}.${thumbprint}`;
146
+ const signature = await signChallengeRaw(privateKey, challengeData);
147
+ const confirmResult = await confirmKey(
148
+ this.brokerUrl,
149
+ this.token,
150
+ regResult.bot_key_id,
151
+ regResult.challenge,
152
+ signature
153
+ );
154
+ if (!confirmResult.key_confirmed) {
155
+ throw new Error("Key enrollment failed: confirmation was rejected");
156
+ }
157
+ console.error("[auth] Key enrollment confirmed");
158
+ await saveKeystore(
159
+ privateKey,
160
+ publicJwk,
161
+ regResult.bot_key_id,
162
+ this.brokerUrl,
163
+ this.keystorePath
164
+ );
165
+ this.privateKey = privateKey;
166
+ this.publicJwk = publicJwk;
167
+ this.kid = regResult.bot_key_id;
168
+ return {
169
+ privateKey,
170
+ publicJwk,
171
+ kid: regResult.bot_key_id
172
+ };
173
+ }
174
+ getPrivateKey() {
175
+ if (!this.privateKey) throw new Error("Not enrolled yet");
176
+ return this.privateKey;
177
+ }
178
+ getPublicJwk() {
179
+ if (!this.publicJwk) throw new Error("Not enrolled yet");
180
+ return this.publicJwk;
181
+ }
182
+ getKid() {
183
+ if (!this.kid) throw new Error("Not enrolled yet");
184
+ return this.kid;
185
+ }
186
+ };
187
+
188
+ // src/gateway-client.ts
189
+ import crypto3 from "crypto";
190
+
191
+ // src/auth/dpop.ts
192
+ import { SignJWT, base64url } from "jose";
193
+ import crypto2 from "crypto";
194
+ async function createDPoPProof(privateKey, kid, method, url, nonce, accessToken) {
195
+ const payload = {
196
+ htm: method,
197
+ htu: url,
198
+ jti: crypto2.randomUUID()
199
+ };
200
+ if (nonce) {
201
+ payload.nonce = nonce;
202
+ }
203
+ if (accessToken) {
204
+ payload.ath = computeAth(accessToken);
205
+ }
206
+ const header = {
207
+ typ: "dpop+jwt",
208
+ alg: "ES256",
209
+ jwk: { kid }
210
+ };
211
+ return new SignJWT(payload).setProtectedHeader(header).setIssuedAt().sign(privateKey);
212
+ }
213
+ function computeAth(accessToken) {
214
+ const hash = crypto2.createHash("sha256").update(accessToken).digest();
215
+ return base64url.encode(hash);
216
+ }
217
+
218
+ // src/error-translator.ts
219
+ function translateGovernanceResult(result) {
220
+ const governance = result._meta?.governance;
221
+ if (!governance) return result;
222
+ if (governance.decision === "deny") {
223
+ return formatDenied(governance);
224
+ }
225
+ if (governance.decision === "require_approval") {
226
+ return formatApprovalRequired(governance);
227
+ }
228
+ return result;
229
+ }
230
+ function formatApprovalRequired(governance) {
231
+ const rules = formatRules(governance.triggered_guardrails);
232
+ const text = `APPROVAL REQUIRED: This action needs user approval before executing. Approval ID: ${governance.approval_id}. Expires: ${governance.expires_at ?? "unknown"}. Triggered rules: ${rules}. The user has been notified. Call kojee_check_approval with this approval_id to check the status.`;
233
+ return {
234
+ content: [{ type: "text", text }],
235
+ isError: true
236
+ };
237
+ }
238
+ function formatDenied(governance) {
239
+ const rules = formatRules(governance.triggered_guardrails);
240
+ const text = `DENIED: This action was blocked by governance policy. Triggered rules: ${rules}. This cannot proceed \u2014 modify your request or ask the user to adjust governance rules.`;
241
+ return {
242
+ content: [{ type: "text", text }],
243
+ isError: true
244
+ };
245
+ }
246
+ function translateHttpError(status, errorCode, _trigger) {
247
+ if (status === 401) {
248
+ if (errorCode === "use_dpop_nonce") {
249
+ return null;
250
+ }
251
+ if (errorCode === "invalid_dpop_proof") {
252
+ return makeError(
253
+ "Authentication failed. The proxy will attempt to re-enroll. If this persists, regenerate your gateway token."
254
+ );
255
+ }
256
+ if (errorCode === "key_enrollment_required") {
257
+ return null;
258
+ }
259
+ return makeError(
260
+ "Gateway token is invalid or expired. Generate a new one."
261
+ );
262
+ }
263
+ if (status === 403 && errorCode === "step_up_required") {
264
+ return makeError(
265
+ "Device re-authorization required. The user has been notified but has not yet approved. Try again later."
266
+ );
267
+ }
268
+ if (status === 429) {
269
+ return makeError(
270
+ "Rate limit exceeded. Wait before making more requests."
271
+ );
272
+ }
273
+ if (status >= 500) {
274
+ return makeError(
275
+ "Kojee gateway encountered an error. Try again."
276
+ );
277
+ }
278
+ return null;
279
+ }
280
+ function translateJsonRpcError(error) {
281
+ const msg = error.message ?? "";
282
+ const msgLower = msg.toLowerCase();
283
+ switch (error.code) {
284
+ case -32601:
285
+ return makeError(
286
+ `Tool not available. It may have been removed or is not connected. Check your connected services in the Kojee dashboard.`
287
+ );
288
+ case -32602:
289
+ return makeError(msg || "Invalid parameters for this tool call.");
290
+ case -32603: {
291
+ if (msgLower.includes("multiple accounts connected")) {
292
+ return makeError(msg);
293
+ }
294
+ if (msgLower.includes("not connected")) {
295
+ return makeError(
296
+ "The service is not connected. Connect it in the Kojee dashboard."
297
+ );
298
+ }
299
+ if (msgLower.includes("scope") && msgLower.includes("access")) {
300
+ return makeError(
301
+ "Token doesn't have access to this tool. Update scopes in the Kojee dashboard."
302
+ );
303
+ }
304
+ if (msgLower.includes("invalid_grant") || msgLower.includes("token refresh failed") || msgLower.includes("re-authorization") || msgLower.includes("reauthorization")) {
305
+ const serviceMatch = msg.match(
306
+ /(?:for|connected for)\s+(\w[\w-]*)/i
307
+ );
308
+ const service = serviceMatch ? serviceMatch[1] : "service";
309
+ return makeError(
310
+ `The ${service} connection needs re-authorization. Ask the user to reconnect it in the Kojee dashboard.`
311
+ );
312
+ }
313
+ return makeError(msg || "An internal error occurred on the gateway.");
314
+ }
315
+ case -32600:
316
+ return makeError(
317
+ "Unexpected response from gateway. This may be a temporary issue."
318
+ );
319
+ case -32e3:
320
+ return makeError(
321
+ "Rate limit exceeded. Wait before making more requests."
322
+ );
323
+ default:
324
+ return makeError(msg || "An unknown error occurred.");
325
+ }
326
+ }
327
+ function translateNetworkError(_error) {
328
+ return makeError(
329
+ "Cannot reach Kojee gateway. Check your connection."
330
+ );
331
+ }
332
+ function translateToolCallResult(result) {
333
+ if (result._meta?.governance) {
334
+ return translateGovernanceResult(result);
335
+ }
336
+ return result;
337
+ }
338
+ function formatRules(guardrails) {
339
+ if (!guardrails || guardrails.length === 0) return "unknown";
340
+ return guardrails.join(", ");
341
+ }
342
+ function makeError(text) {
343
+ return {
344
+ content: [{ type: "text", text }],
345
+ isError: true
346
+ };
347
+ }
348
+
349
+ // src/gateway-client.ts
350
+ var STEP_UP_POLL_INTERVAL_MS = 5e3;
351
+ var STEP_UP_MAX_TIMEOUT_MS = 3e5;
352
+ var GatewayClient = class {
353
+ constructor(brokerUrl, token, privateKey, kid, sessionId) {
354
+ this.brokerUrl = brokerUrl;
355
+ this.token = token;
356
+ this.privateKey = privateKey;
357
+ this.kid = kid;
358
+ this.endpoint = `${brokerUrl}/mcp/messages/${sessionId}/`;
359
+ }
360
+ brokerUrl;
361
+ token;
362
+ privateKey;
363
+ kid;
364
+ currentNonce;
365
+ requestCounter = 0;
366
+ endpoint;
367
+ /**
368
+ * Derive a deterministic session ID from the gateway token.
369
+ * session_id = sha256(token + "proxy").slice(0, 16)
370
+ */
371
+ static deriveSessionId(token) {
372
+ const hash = crypto3.createHash("sha256").update(token + "proxy").digest("hex");
373
+ return hash.slice(0, 16);
374
+ }
375
+ /**
376
+ * Send a JSON-RPC 2.0 request to the gateway, handling DPoP auth,
377
+ * nonce retry, and step-up retry transparently.
378
+ */
379
+ async sendRpc(method, params = {}) {
380
+ const rpcRequest = {
381
+ jsonrpc: "2.0",
382
+ id: ++this.requestCounter,
383
+ method,
384
+ params
385
+ };
386
+ return this.executeWithRetries(rpcRequest);
387
+ }
388
+ async executeWithRetries(rpcRequest) {
389
+ let response;
390
+ try {
391
+ response = await this.sendHttpRequest(rpcRequest);
392
+ } catch (err) {
393
+ return translateNetworkError(err);
394
+ }
395
+ this.trackNonce(response);
396
+ if (response.status === 401) {
397
+ const body = await this.tryParseErrorBody(response);
398
+ if (body?.error === "use_dpop_nonce") {
399
+ console.error("[gateway] Nonce expired, retrying with fresh nonce...");
400
+ try {
401
+ response = await this.sendHttpRequest(rpcRequest);
402
+ } catch (err) {
403
+ return translateNetworkError(err);
404
+ }
405
+ this.trackNonce(response);
406
+ } else {
407
+ const translated = translateHttpError(401, body?.error);
408
+ if (translated) return translated;
409
+ }
410
+ }
411
+ if (response.status === 403) {
412
+ const body = await this.tryParseErrorBody(response);
413
+ if (body?.error === "step_up_required") {
414
+ return this.handleStepUp(rpcRequest, body.trigger);
415
+ }
416
+ const translated = translateHttpError(403, body?.error, body?.trigger);
417
+ if (translated) return translated;
418
+ }
419
+ if (!response.ok) {
420
+ const body = await this.tryParseErrorBody(response);
421
+ const translated = translateHttpError(response.status, body?.error);
422
+ if (translated) return translated;
423
+ return {
424
+ content: [{ type: "text", text: `Gateway error: ${response.status}` }],
425
+ isError: true
426
+ };
427
+ }
428
+ const rpcResponse = await response.json();
429
+ if (rpcResponse.error) {
430
+ return translateJsonRpcError(rpcResponse.error);
431
+ }
432
+ const result = rpcResponse.result;
433
+ return result ?? { content: [{ type: "text", text: "No result" }] };
434
+ }
435
+ async sendHttpRequest(rpcRequest) {
436
+ const proof = await createDPoPProof(
437
+ this.privateKey,
438
+ this.kid,
439
+ "POST",
440
+ this.endpoint,
441
+ this.currentNonce,
442
+ this.token
443
+ );
444
+ return fetch(this.endpoint, {
445
+ method: "POST",
446
+ headers: {
447
+ "Content-Type": "application/json",
448
+ Authorization: `DPoP ${this.token}`,
449
+ DPoP: proof
450
+ },
451
+ body: JSON.stringify(rpcRequest)
452
+ });
453
+ }
454
+ /**
455
+ * Handle step-up retry: poll with backoff until user approves
456
+ * or timeout is reached.
457
+ */
458
+ async handleStepUp(rpcRequest, trigger) {
459
+ console.error(
460
+ `[gateway] Device re-authorization required (${trigger ?? "unknown"}). Waiting for user approval...`
461
+ );
462
+ const deadline = Date.now() + STEP_UP_MAX_TIMEOUT_MS;
463
+ while (Date.now() < deadline) {
464
+ await sleep(STEP_UP_POLL_INTERVAL_MS);
465
+ let response;
466
+ try {
467
+ response = await this.sendHttpRequest(rpcRequest);
468
+ } catch {
469
+ continue;
470
+ }
471
+ this.trackNonce(response);
472
+ if (response.status === 403) {
473
+ const body2 = await this.tryParseErrorBody(response);
474
+ if (body2?.error === "step_up_required") {
475
+ console.error("[gateway] Still waiting for step-up approval...");
476
+ continue;
477
+ }
478
+ }
479
+ if (response.ok) {
480
+ const rpcResponse = await response.json();
481
+ if (rpcResponse.error) {
482
+ return translateJsonRpcError(rpcResponse.error);
483
+ }
484
+ const result = rpcResponse.result;
485
+ return result ?? { content: [{ type: "text", text: "No result" }] };
486
+ }
487
+ const body = await this.tryParseErrorBody(response);
488
+ const translated = translateHttpError(response.status, body?.error);
489
+ if (translated) return translated;
490
+ }
491
+ return {
492
+ content: [
493
+ {
494
+ type: "text",
495
+ text: "Device re-authorization was not approved within 5 minutes. Try again later."
496
+ }
497
+ ],
498
+ isError: true
499
+ };
500
+ }
501
+ trackNonce(response) {
502
+ const nonce = response.headers.get("DPoP-Nonce");
503
+ if (nonce) {
504
+ this.currentNonce = nonce;
505
+ }
506
+ }
507
+ async tryParseErrorBody(response) {
508
+ try {
509
+ return await response.json();
510
+ } catch {
511
+ return null;
512
+ }
513
+ }
514
+ };
515
+ function sleep(ms) {
516
+ return new Promise((resolve) => setTimeout(resolve, ms));
517
+ }
518
+
519
+ // src/tool-registry.ts
520
+ var KOJEE_PREFIX = "kojee_";
521
+ var isBuiltinTool = (name) => name.startsWith(KOJEE_PREFIX);
522
+ var ToolRegistry = class {
523
+ constructor(gateway) {
524
+ this.gateway = gateway;
525
+ }
526
+ gateway;
527
+ /** Map from agent-facing name (prefixed) to full tool definition */
528
+ tools = /* @__PURE__ */ new Map();
529
+ /** Map from agent-facing name to gateway name */
530
+ nameMap = /* @__PURE__ */ new Map();
531
+ /**
532
+ * Discover tools from the gateway:
533
+ * 1. Call tools/list (lean) — returns names + descriptions for all tools.
534
+ * 2. Separate built-in kojee_* tools (already prefixed) from connector tools.
535
+ * 3. Call tools/get_schema for connector tool names to get full inputSchema.
536
+ * 4. Register everything with the kojee_ prefix mapping.
537
+ */
538
+ async discoverTools() {
539
+ console.error("[tools] Fetching tool list from gateway...");
540
+ const listResult = await this.gateway.sendRpc("tools/list", {});
541
+ const toolList = listResult?.tools;
542
+ if (!toolList || !Array.isArray(toolList)) {
543
+ console.error("[tools] No tools returned from gateway");
544
+ return;
545
+ }
546
+ console.error(`[tools] Found ${toolList.length} tools`);
547
+ const builtinTools = [];
548
+ const connectorToolNames = [];
549
+ for (const tool of toolList) {
550
+ if (isBuiltinTool(tool.name)) {
551
+ builtinTools.push(tool);
552
+ } else {
553
+ connectorToolNames.push(tool.name);
554
+ }
555
+ }
556
+ if (builtinTools.length > 0) {
557
+ const builtinSchemaResult = await this.gateway.sendRpc("tools/get_schema", {
558
+ names: builtinTools.map((t) => t.name)
559
+ });
560
+ const fullBuiltins = builtinSchemaResult?.tools;
561
+ if (fullBuiltins && Array.isArray(fullBuiltins)) {
562
+ for (const tool of fullBuiltins) {
563
+ this.registerTool(tool);
564
+ }
565
+ } else {
566
+ for (const tool of builtinTools) {
567
+ this.registerTool({
568
+ name: tool.name,
569
+ description: tool.description,
570
+ inputSchema: tool.inputSchema ?? { type: "object", properties: {} }
571
+ });
572
+ }
573
+ }
574
+ }
575
+ if (connectorToolNames.length > 0) {
576
+ console.error(`[tools] Fetching schemas for ${connectorToolNames.length} connector tools...`);
577
+ const schemaResult = await this.gateway.sendRpc("tools/get_schema", {
578
+ names: connectorToolNames
579
+ });
580
+ const fullTools = schemaResult?.tools;
581
+ const notFound = schemaResult?.not_found;
582
+ if (notFound && notFound.length > 0) {
583
+ console.error(`[tools] Warning: schemas not found for: ${notFound.join(", ")}`);
584
+ }
585
+ if (fullTools && Array.isArray(fullTools)) {
586
+ for (const tool of fullTools) {
587
+ this.registerTool(tool);
588
+ }
589
+ } else {
590
+ console.error("[tools] Schema fetch failed, using lean definitions");
591
+ for (const name of connectorToolNames) {
592
+ const lean = toolList.find((t) => t.name === name);
593
+ if (lean) {
594
+ this.registerTool({
595
+ name: lean.name,
596
+ description: lean.description,
597
+ inputSchema: { type: "object", properties: {} }
598
+ });
599
+ }
600
+ }
601
+ }
602
+ }
603
+ console.error(`[tools] Registered ${this.tools.size} tools`);
604
+ }
605
+ /**
606
+ * Register a single tool, applying the kojee_ prefix if needed.
607
+ */
608
+ registerTool(tool) {
609
+ const gatewayName = tool.name;
610
+ const agentName = gatewayName.startsWith(KOJEE_PREFIX) ? gatewayName : `${KOJEE_PREFIX}${gatewayName}`;
611
+ this.nameMap.set(agentName, gatewayName);
612
+ this.tools.set(agentName, {
613
+ ...tool,
614
+ name: agentName
615
+ });
616
+ }
617
+ /**
618
+ * Get all registered tools as MCP tool definitions for the agent.
619
+ */
620
+ getTools() {
621
+ return Array.from(this.tools.values());
622
+ }
623
+ /**
624
+ * Resolve an agent-facing tool name to its gateway name.
625
+ * Returns null if the tool is not registered.
626
+ */
627
+ resolveGatewayName(agentName) {
628
+ return this.nameMap.get(agentName) ?? null;
629
+ }
630
+ /**
631
+ * Call a tool through the gateway, handling name mapping.
632
+ */
633
+ async callTool(agentName, args) {
634
+ const gatewayName = this.resolveGatewayName(agentName);
635
+ if (!gatewayName) {
636
+ return {
637
+ content: [
638
+ {
639
+ type: "text",
640
+ text: `Tool '${agentName}' not found. It may have been removed or your token lacks access.`
641
+ }
642
+ ],
643
+ isError: true
644
+ };
645
+ }
646
+ return this.gateway.sendRpc("tools/call", {
647
+ name: gatewayName,
648
+ arguments: args
649
+ });
650
+ }
651
+ };
652
+
653
+ // src/server.ts
654
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
655
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
656
+ import {
657
+ ListToolsRequestSchema,
658
+ CallToolRequestSchema
659
+ } from "@modelcontextprotocol/sdk/types.js";
660
+ function createMcpServer(registry) {
661
+ const server = new Server(
662
+ {
663
+ name: "kojee-mcp",
664
+ version: "0.1.0"
665
+ },
666
+ {
667
+ capabilities: {
668
+ tools: {}
669
+ }
670
+ }
671
+ );
672
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
673
+ const tools = registry.getTools().map((t) => ({
674
+ name: t.name,
675
+ description: t.description,
676
+ inputSchema: t.inputSchema
677
+ }));
678
+ return { tools };
679
+ });
680
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
681
+ const { name, arguments: args } = request.params;
682
+ const rawResult = await registry.callTool(name, args ?? {});
683
+ const result = translateToolCallResult(rawResult);
684
+ return {
685
+ content: result.content,
686
+ isError: result.isError
687
+ };
688
+ });
689
+ return server;
690
+ }
691
+ async function startMcpServer(server) {
692
+ const transport = new StdioServerTransport();
693
+ await server.connect(transport);
694
+ console.error("[mcp] Server started on stdio transport");
695
+ }
696
+
697
+ // src/index.ts
698
+ var DEFAULT_KEYSTORE_PATH = path2.join(
699
+ process.env["HOME"] ?? "~",
700
+ ".kojee",
701
+ "keypair.json"
702
+ );
703
+ async function startProxy(config) {
704
+ const keystorePath = config.keystorePath || DEFAULT_KEYSTORE_PATH;
705
+ console.error(`[kojee-mcp] Starting proxy for ${config.url}`);
706
+ const auth = new AuthModule(config.token, config.url, keystorePath);
707
+ const keyPair = await auth.ensureEnrolled();
708
+ const sessionId = GatewayClient.deriveSessionId(config.token);
709
+ console.error(`[kojee-mcp] Session: ${sessionId}`);
710
+ const gateway = new GatewayClient(
711
+ config.url,
712
+ config.token,
713
+ keyPair.privateKey,
714
+ keyPair.kid,
715
+ sessionId
716
+ );
717
+ const registry = new ToolRegistry(gateway);
718
+ await registry.discoverTools();
719
+ const toolCount = registry.getTools().length;
720
+ console.error(
721
+ `[kojee-mcp] Ready \u2014 ${toolCount} tools proxied from ${config.url}`
722
+ );
723
+ const server = createMcpServer(registry);
724
+ await startMcpServer(server);
725
+ }
726
+
727
+ export {
728
+ startProxy
729
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ startProxy
4
+ } from "./chunk-UDF4BJS5.js";
5
+
6
+ // src/cli.ts
7
+ import { Command } from "commander";
8
+ import path from "path";
9
+ var DEFAULT_KEYSTORE_PATH = path.join(
10
+ process.env["HOME"] ?? "~",
11
+ ".kojee",
12
+ "keypair.json"
13
+ );
14
+ var program = new Command().name("kojee-mcp").description(
15
+ "Local MCP proxy for Kojee \u2014 handles DPoP auth, tool discovery, and governance transparently"
16
+ ).version("0.1.0").requiredOption(
17
+ "--token <token>",
18
+ "Gateway token (gw_...)"
19
+ ).requiredOption(
20
+ "--url <url>",
21
+ "Broker base URL (e.g. https://kojee.ai)"
22
+ ).option(
23
+ "--keystore-path <path>",
24
+ "Path to persisted keypair file",
25
+ DEFAULT_KEYSTORE_PATH
26
+ ).action(async (opts) => {
27
+ if (!opts.token.startsWith("gw_")) {
28
+ console.error(
29
+ "Warning: Gateway token does not start with 'gw_'. Ensure you are using a valid gateway token."
30
+ );
31
+ }
32
+ const url = opts.url.replace(/\/+$/, "");
33
+ try {
34
+ await startProxy({
35
+ token: opts.token,
36
+ url,
37
+ keystorePath: opts.keystorePath
38
+ });
39
+ } catch (err) {
40
+ console.error("[kojee-mcp] Fatal error:", err);
41
+ process.exit(1);
42
+ }
43
+ });
44
+ program.parse();
@@ -0,0 +1,19 @@
1
+ interface ProxyConfig {
2
+ token: string;
3
+ url: string;
4
+ keystorePath: string;
5
+ }
6
+
7
+ /**
8
+ * Bootstrap and start the Kojee MCP proxy.
9
+ *
10
+ * Startup sequence:
11
+ * 1. Enroll keypair (or load existing)
12
+ * 2. Derive session ID
13
+ * 3. Create gateway client
14
+ * 4. Discover tools from gateway
15
+ * 5. Start MCP stdio server
16
+ */
17
+ declare function startProxy(config: ProxyConfig): Promise<void>;
18
+
19
+ export { type ProxyConfig, startProxy };
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import {
2
+ startProxy
3
+ } from "./chunk-UDF4BJS5.js";
4
+ export {
5
+ startProxy
6
+ };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "kojee-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Local MCP proxy for Kojee — handles DPoP auth, tool discovery, and governance transparently",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": { "kojee-mcp": "dist/cli.js" },
8
+ "scripts": {
9
+ "build": "tsup src/cli.ts src/index.ts --format esm --dts --clean",
10
+ "dev": "tsup src/cli.ts --format esm --watch",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "files": ["dist"],
14
+ "keywords": ["mcp", "kojee", "ai-governance", "agent-tools", "dpop"],
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "latest",
18
+ "jose": "^5.0.0",
19
+ "commander": "^12.0.0",
20
+ "zod": "^3.22.0"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.4.0",
24
+ "@types/node": "^20.0.0",
25
+ "tsup": "^8.0.0",
26
+ "vitest": "^1.0.0"
27
+ }
28
+ }