kojee-mcp 0.2.2 → 0.5.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/README.md +151 -7
- package/dist/chunk-BJMASMKX.js +41 -0
- package/dist/chunk-E26AHU6J.js +27 -0
- package/dist/chunk-GBOTBYEP.js +34 -0
- package/dist/{chunk-QKAUM3TR.js → chunk-LCFCCWMM.js} +359 -74
- package/dist/chunk-LSUB6QMP.js +75 -0
- package/dist/chunk-QB22PD6T.js +358 -0
- package/dist/chunk-VLZADEFC.js +247 -0
- package/dist/chunk-W6YRLSD4.js +143 -0
- package/dist/cli.js +209 -17
- package/dist/doctor-GILTOH2R.js +222 -0
- package/dist/event-log-R6VW6GAF.js +17 -0
- package/dist/event-queue-5YVJFR3E.js +43 -0
- package/dist/hook-server-QF5JVUHV.js +99 -0
- package/dist/index.d.ts +0 -13
- package/dist/index.js +4 -1
- package/dist/install-D2HIPOMT.js +183 -0
- package/dist/paired-config-RB4SABOS.js +10 -0
- package/dist/resubscribe-SLZNA76S.js +59 -0
- package/dist/session-discovery-QE5TTAPS.js +26 -0
- package/dist/stop-hook-VLQS6QPR.js +118 -0
- package/dist/tail-stream-UZ42UIWO.js +161 -0
- package/dist/user-prompt-submit-hook-C42DPDBO.js +54 -0
- package/package.json +11 -13
package/README.md
CHANGED
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
# kojee-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **First time using Kojee Tandem in Claude Code?** See [docs/getting-started-tandem.md](docs/getting-started-tandem.md) for the 3-command setup + verification walkthrough.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
There are two ways to connect Kojee to an MCP-capable agent:
|
|
6
6
|
|
|
7
|
-
1. **
|
|
8
|
-
2. **
|
|
9
|
-
3. **Tools appear automatically** -- all Kojee tools your token has access to are exposed directly (e.g. `gmail_send_email`, `github_list_repos`, `calendar_create_event`). Agents call them like any native MCP tool — no discovery step, no wrapper, no indirection.
|
|
7
|
+
1. **Mobile, web & desktop (recommended)** — paste the Kojee MCP URL into the app's "Add custom connector" dialog. The app handles OAuth login and consent. No local install. Works on Claude (web/desktop/iOS/Android) and ChatGPT (web/desktop/iOS/Android with Developer Mode enabled).
|
|
8
|
+
2. **Power user / local proxy with DPoP** — run this stdio proxy locally with a gateway token. Strongest wire-level security (DPoP proof-of-possession, RFC 9449), but requires Node and a token you generate manually in the Kojee dashboard.
|
|
10
9
|
|
|
11
|
-
##
|
|
10
|
+
## Mobile, Web & Desktop (Recommended)
|
|
11
|
+
|
|
12
|
+
Paste this URL into the app's connector dialog:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
https://api.kojee.ai/mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- **Claude (any surface):** Settings → Connectors → Add custom connector → paste URL → Connect → log in to Kojee → approve scopes.
|
|
19
|
+
- **ChatGPT (any surface, Developer Mode on):** Settings → Connectors → New connector → paste URL → choose OAuth → Connect.
|
|
20
|
+
|
|
21
|
+
The OAuth login + consent flow takes care of everything — no token paste, no config file, no Node required. Tokens are short-lived (1 h) and audience-bound to the MCP URL (RFC 8707). Refresh tokens rotate on every use.
|
|
22
|
+
|
|
23
|
+
### Remote MCP URL details
|
|
24
|
+
|
|
25
|
+
| Field | Value |
|
|
26
|
+
|---|---|
|
|
27
|
+
| MCP URL | `https://api.kojee.ai/mcp` |
|
|
28
|
+
| Transport | Streamable HTTP (POST + GET-SSE) |
|
|
29
|
+
| Auth | OAuth 2.1 + Dynamic Client Registration (RFC 7591) |
|
|
30
|
+
| Discovery | `https://api.kojee.ai/.well-known/oauth-protected-resource` |
|
|
31
|
+
| Scopes | `mcp:tools`, `mcp:read` |
|
|
32
|
+
|
|
33
|
+
## Power User: Local Proxy with DPoP
|
|
34
|
+
|
|
35
|
+
The `kojee-mcp` stdio proxy holds a `gw_` gateway token + ES256 keypair locally and signs every request with DPoP (RFC 9449). This survives token-in-transit theft (TLS-terminating proxy logs, etc.) — strictly stronger wire-level posture than bearer-only OAuth, but requires a long-lived token.
|
|
12
36
|
|
|
13
37
|
### Claude Code / Claude Desktop
|
|
14
38
|
|
|
@@ -25,7 +49,7 @@ kojee-mcp is a local MCP proxy that lets any MCP-capable agent use Kojee-managed
|
|
|
25
49
|
|
|
26
50
|
### Generic MCP Client
|
|
27
51
|
|
|
28
|
-
Any MCP client that supports stdio transports can use kojee-mcp
|
|
52
|
+
Any MCP client that supports stdio transports can use kojee-mcp:
|
|
29
53
|
|
|
30
54
|
```bash
|
|
31
55
|
npx kojee-mcp --token gw_abc123... --url https://kojee.ai
|
|
@@ -46,6 +70,126 @@ kojee-mcp --token gw_abc123... --url https://kojee.ai
|
|
|
46
70
|
| `--url` | yes | -- | Kojee broker URL (e.g. `https://kojee.ai`) |
|
|
47
71
|
| `--keystore-path` | no | `~/.kojee/keypair.json` | Path for DPoP keypair storage |
|
|
48
72
|
|
|
73
|
+
## Pair Mode (Tandem)
|
|
74
|
+
|
|
75
|
+
If your token comes from the Kojee Tandem web wizard (a "pair code"), you can persist it for future runs:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# One-time setup:
|
|
79
|
+
npx kojee-mcp pair ABCD-1234 --url https://rosie-server.kojee.net
|
|
80
|
+
npx kojee-mcp init # adds the MCP entry to EVERY detected Claude installation
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`init` writes to both `~/.claude.json` (terminal `claude` CLI) and `~/Library/Application Support/Claude/claude_desktop_config.json` (Claude.app on macOS — Windows / Linux paths in the spec) if either exists. The canonical MCP entry includes `env.KOJEE_RUNTIME=claude-code` so the proxy correctly identifies as Claude Code even when the desktop app strips environment variables from MCP subprocesses.
|
|
84
|
+
|
|
85
|
+
**Important for Claude.app users:** existing agent-mode sessions snapshot the MCP config at creation time and won't pick up changes from `init`. Start a NEW session (not a resumed one) to use the updated kojee config.
|
|
86
|
+
|
|
87
|
+
Subsequent runs of `claude` will find kojee automatically. The proxy writes credentials to `~/.kojee/config.json` (mode 0600). Existing `--token` users are unaffected.
|
|
88
|
+
|
|
89
|
+
To remove kojee from Claude:
|
|
90
|
+
```bash
|
|
91
|
+
npx kojee-mcp init --uninstall
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Claude Code Channels Support
|
|
95
|
+
|
|
96
|
+
When this proxy detects it's running under Claude Code, it declares the `claude/channel` capability and pushes Tandem messages directly into the Claude session via `notifications/claude/channel`. Other MCP clients (Cursor, Codex, Openclaw, etc.) see no behavior change.
|
|
97
|
+
|
|
98
|
+
**Runtime detection (3 tiers, first match wins):**
|
|
99
|
+
1. `KOJEE_RUNTIME=claude-code` env var (set by `kojee-mcp init` in the MCP entry's `env` block — survives Claude.app's env strip)
|
|
100
|
+
2. `CLAUDE_CODE_SESSION_ID` env var (set by the terminal `claude` CLI; **not** forwarded by Claude.app to MCP stdio servers)
|
|
101
|
+
3. Process-ancestry walk for a parent process whose command line matches `\bclaude\b` (handles fresh installs where neither env var is present)
|
|
102
|
+
|
|
103
|
+
CC Channels are in research preview — you must launch CC with `claude --dangerously-load-development-channels server:kojee` until kojee is on the official allowlist.
|
|
104
|
+
|
|
105
|
+
## Hooks Support (always-on, no allowlist required)
|
|
106
|
+
|
|
107
|
+
While Channels is in research preview and gated behind Anthropic's allowlist, kojee also supports the standard Claude Code hooks system as a complementary wake path. Hooks require no special launch flag.
|
|
108
|
+
|
|
109
|
+
If you used `kojee-mcp init` above, hooks are already installed. The standalone command is:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npx kojee-mcp install-hooks
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This writes to **`~/.claude/settings.json`** (the file CC reads for hooks — NOT `~/.claude.json`, which is the MCP-servers file). Foreign hooks already in `settings.json` (e.g. babysitter wrappers) are preserved as siblings.
|
|
116
|
+
|
|
117
|
+
Restart Claude Code. Tandem events arriving between turns (Stop hook) or while you're typing a new prompt (UserPromptSubmit hook) will be injected into the agent's context the next time it acts.
|
|
118
|
+
|
|
119
|
+
To remove:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npx kojee-mcp install-hooks --uninstall
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Override the default `~/.claude/settings.json` path with `--hooks-path <path>` if needed.
|
|
126
|
+
|
|
127
|
+
If both Channels (dangerous-flag launched) and hooks are active, the proxy deduplicates so events arrive exactly once. Hook-delivered events are also suppressed when Monitor (below) has already delivered them push-style.
|
|
128
|
+
|
|
129
|
+
## Waiting on Tandem Peers
|
|
130
|
+
|
|
131
|
+
When kojee-mcp is running under Claude Code, the proxy writes one line per
|
|
132
|
+
Tandem message to a per-session log file under the OS's temp directory
|
|
133
|
+
(`os.tmpdir()` — `/tmp` on Linux, `/var/folders/…` on macOS, `%TEMP%` on
|
|
134
|
+
Windows). The agent watches this log via Claude Code's built-in `Monitor`
|
|
135
|
+
tool — spawned once at the start of each session:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
Monitor({
|
|
139
|
+
command: `npx kojee-mcp tail "${eventLogPath}"`,
|
|
140
|
+
persistent: true,
|
|
141
|
+
description: "kojee Tandem events",
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`kojee-mcp tail` is a portable line-streamer shipped with this proxy —
|
|
146
|
+
works the same on macOS, Linux, and Windows. The proxy interpolates the
|
|
147
|
+
resolved `eventLogPath` into the Channel-`instructions` string so the
|
|
148
|
+
agent receives a ready-to-run command.
|
|
149
|
+
|
|
150
|
+
The `<discoveryKey>` embedded in the log filename is computed from `sha256(CLAUDE_PROJECT_DIR).slice(0,12) + '-' + <ccPid>` where `ccPid` is the parent Claude Code process. (This replaces the pre-v0.4 scheme that used `CLAUDE_CODE_SESSION_ID`, which Claude.app doesn't forward to MCP servers.) The matching `~/.kojee/sessions/cc-<discoveryKey>.json` discovery file lets the Stop / UserPromptSubmit hooks find this proxy via the same independent derivation.
|
|
151
|
+
|
|
152
|
+
Each appended event line arrives as a separate spontaneous wake notification. The agent stays free to chat with the user between events; CC delivers each event from idle as it arrives.
|
|
153
|
+
|
|
154
|
+
**Stop hook always long-polls** (up to 30s): the queue's `markMonitorDelivered` dedup ensures every event is delivered exactly once across the Channel / Monitor / Stop-hook paths, so the hook doesn't need to probe whether a Monitor is running. If the agent skipped the session-start Monitor spawn, the long-poll backstop still catches events; if Monitor is running, the dedup filters out anything already delivered push-style.
|
|
155
|
+
|
|
156
|
+
Channel notifications (when available — see "Claude Code Channels Support" above) supplement this with mid-turn `<channel>` tag delivery, but Monitor is the default no-allowlist wake path that works for every user.
|
|
157
|
+
|
|
158
|
+
**Cross-platform support:** the proxy core, the `kojee-mcp tail` Monitor command, and the Channel wake path are portable across macOS, Linux, and Windows — path resolution uses `os.homedir()` / `os.tmpdir()` and process-ancestry detection uses the pure-JS `ps-list` package. Claude Code hook invocation on Windows is pending end-to-end verification; on macOS and Linux it is exercised by CI.
|
|
159
|
+
|
|
160
|
+
For one-shot blocking waits (return as soon as a single reply arrives), call `tandem_listen(tandem_id, since=cursor, timeout_ms=N)` instead.
|
|
161
|
+
|
|
162
|
+
## Backend SSE Wire Compatibility
|
|
163
|
+
|
|
164
|
+
The proxy normalizes incoming SSE events from `/api/v2/tandems/stream` so that the on-the-wire payload shape (which currently differs from the spec'd `TandemEvent`) is mapped transparently:
|
|
165
|
+
|
|
166
|
+
| Wire field | Internal field |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `message_id` | `id` |
|
|
169
|
+
| `sender.principal_id` | `from.principal` |
|
|
170
|
+
| `sender.agent_id` | `from.agent_id` |
|
|
171
|
+
| top-level `body` | `content.body` |
|
|
172
|
+
| top-level `format` | `content.format` |
|
|
173
|
+
| (missing) `displayname` | synthesized as `principal:<first-8-chars-of-principal_id>` |
|
|
174
|
+
|
|
175
|
+
The normalizer also accepts canonical (spec-aligned) payloads unchanged, so the proxy works against either shape during any future backend migration.
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
Run tests:
|
|
180
|
+
```bash
|
|
181
|
+
npm install
|
|
182
|
+
npm test
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The test suite uses an in-process stub broker (`dev-tools/stub-broker.ts`) — no external dependencies, no Docker, no MongoDB. CI runs the same way.
|
|
186
|
+
|
|
187
|
+
Run the stub manually for hands-on CC verification:
|
|
188
|
+
```bash
|
|
189
|
+
npm run dev:stub
|
|
190
|
+
# Stub listens on http://localhost:8765
|
|
191
|
+
```
|
|
192
|
+
|
|
49
193
|
## How Approvals Work
|
|
50
194
|
|
|
51
195
|
Some tools are governed by approval policies configured in the Kojee dashboard. When an agent calls a governed tool:
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/runtime/ancestry.ts
|
|
2
|
+
import psList from "ps-list";
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
async function findClaudeAncestorPid(startPid = process.ppid) {
|
|
5
|
+
let processes;
|
|
6
|
+
try {
|
|
7
|
+
processes = await psList();
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const byPid = /* @__PURE__ */ new Map();
|
|
12
|
+
for (const p of processes) byPid.set(p.pid, p);
|
|
13
|
+
let pid = startPid;
|
|
14
|
+
for (let depth = 0; depth < 20 && pid !== void 0 && pid > 1; depth++) {
|
|
15
|
+
const row = byPid.get(pid);
|
|
16
|
+
if (!row) return null;
|
|
17
|
+
const haystack = `${row.name} ${row.cmd ?? ""}`;
|
|
18
|
+
if (/\bclaude\b/i.test(haystack)) return pid;
|
|
19
|
+
if (row.ppid === void 0 || row.ppid === pid) return null;
|
|
20
|
+
pid = row.ppid;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function deriveDiscoveryKey(projectDir, ccPid) {
|
|
25
|
+
const hasProjectDir = typeof projectDir === "string" && projectDir.length > 0;
|
|
26
|
+
if (!hasProjectDir && ccPid === null) {
|
|
27
|
+
return `orphan-${process.pid}`;
|
|
28
|
+
}
|
|
29
|
+
if (!hasProjectDir) {
|
|
30
|
+
const hash2 = createHash("sha256").update("<no-project>").digest("hex").slice(0, 12);
|
|
31
|
+
return `${hash2}-${ccPid}`;
|
|
32
|
+
}
|
|
33
|
+
const hash = createHash("sha256").update(projectDir).digest("hex").slice(0, 12);
|
|
34
|
+
if (ccPid === null) return `${hash}-orphan`;
|
|
35
|
+
return `${hash}-${ccPid}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
findClaudeAncestorPid,
|
|
40
|
+
deriveDiscoveryKey
|
|
41
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/tandem/recipe.ts
|
|
2
|
+
var SEND_BODY_PARAM = "body";
|
|
3
|
+
function buildMonitorCommand(logPath) {
|
|
4
|
+
return `npx kojee-mcp tail "${logPath}"`;
|
|
5
|
+
}
|
|
6
|
+
function buildMonitorSpawn(logPath) {
|
|
7
|
+
return `Monitor(command=\`${buildMonitorCommand(logPath)}\`, persistent=true, description="kojee Tandem events")`;
|
|
8
|
+
}
|
|
9
|
+
function buildReplyRecipe(event) {
|
|
10
|
+
if (event) {
|
|
11
|
+
return `reply directly with tandem_send(tandem_id="${event.tandem_id}", ${SEND_BODY_PARAM}="...", reply_to="${event.message_id}"); fetch context only if needed via tandem_messages(tandem_id, since=cursor-1)`;
|
|
12
|
+
}
|
|
13
|
+
return `reply directly with tandem_send(tandem_id, ${SEND_BODY_PARAM}, reply_to=<msg>); fetch context only if needed via tandem_messages(tandem_id, since=cursor-1)`;
|
|
14
|
+
}
|
|
15
|
+
function buildCatchUpNote() {
|
|
16
|
+
return "If the event line `cursor=<n>` ever jumps (a gap) or the log resets (the proxy caps and truncates the messages log in place on overflow \u2014 a `status=rotated` line is recorded in the status sibling), you may have missed messages: catch up with tandem_messages(tandem_id, since=<last cursor you saw>).";
|
|
17
|
+
}
|
|
18
|
+
function buildMonitorNudge(logPath) {
|
|
19
|
+
return `[kojee] Tandem events are being logged but no Monitor is reading them \u2014 you may be missing wake notifications. Spawn the watcher once: ${buildMonitorSpawn(logPath)}. Then ${buildReplyRecipe()}.`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
buildMonitorSpawn,
|
|
24
|
+
buildReplyRecipe,
|
|
25
|
+
buildCatchUpNote,
|
|
26
|
+
buildMonitorNudge
|
|
27
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/auth/paired-config.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
function pairedConfigPath() {
|
|
6
|
+
return path.join(os.homedir(), ".kojee", "config.json");
|
|
7
|
+
}
|
|
8
|
+
function loadPairedConfig(filePath = pairedConfigPath()) {
|
|
9
|
+
try {
|
|
10
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function savePairedConfig(filePath, config) {
|
|
17
|
+
const dir = path.dirname(filePath);
|
|
18
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
19
|
+
try {
|
|
20
|
+
fs.chmodSync(dir, 448);
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
24
|
+
try {
|
|
25
|
+
fs.chmodSync(filePath, 384);
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
pairedConfigPath,
|
|
32
|
+
loadPairedConfig,
|
|
33
|
+
savePairedConfig
|
|
34
|
+
};
|