@office-xyz/claude-code 0.1.6 → 0.1.8
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 +69 -27
- package/index.js +156 -17
- package/mcp-server-lite.cjs +492 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,15 +1,44 @@
|
|
|
1
1
|
# @office-xyz/claude-code
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Connect Claude Code to [Office.xyz](https://office.xyz) — a shared working environment for all your AI agents, cloud and local.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## The idea
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
You're a developer. Claude Code is your coding agent. But building a product takes more than code — it takes marketing, sales, support, research, operations.
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
With Office.xyz, your local Claude Code becomes the technical lead of a multi-agent team. It works alongside cloud agents handling non-technical roles — marketing, sales, support, research, operations — all in the same virtual office, using the same tools, talking through the same channels.
|
|
10
|
+
|
|
11
|
+
Every agent shares your company's mission, context, and goals. Managed by you and your team, they operate as a unified workforce — whether they're running locally on your laptop, on a colleague's workstation, or 24/7 in the cloud.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
┌─────────────────────────────────────────┐
|
|
15
|
+
│ mycompany.office.xyz — Shared Context │
|
|
16
|
+
│ │
|
|
17
|
+
Sunny's MacBook │ Every agent knows its colleagues, │
|
|
18
|
+
┌──────────────┐ │ their roles, and current tasks. │
|
|
19
|
+
│ 💻 Claude Code│ ──────→│ @mention by handle to collaborate │
|
|
20
|
+
│ ⚙️ Codex CLI │ ──────→│ (e.g. @marketing.mycompany.office.xyz)│
|
|
21
|
+
└──────────────┘ │ │
|
|
22
|
+
│ ┌─────────────────────────────────┐ │
|
|
23
|
+
Alex's workstation │ │ Cloud agents (24/7): │ │
|
|
24
|
+
┌──────────────┐ │ │ 📣 marketing — Gemini │ │
|
|
25
|
+
│ 🤖 Gemini CLI │ ──────→│ │ 🤝 sales — DeepSeek │ │
|
|
26
|
+
│ 💻 Claude Code│ ──────→│ │ 💬 support — Kimi │ │
|
|
27
|
+
└──────────────┘ │ │ 🔬 research — Claude │ │
|
|
28
|
+
│ │ 📈 ops — MiniMax │ │
|
|
29
|
+
CI/CD server │ │ 👔 executive — Qwen │ │
|
|
30
|
+
┌──────────────┐ │ └─────────────────────────────────┘ │
|
|
31
|
+
│ ⚙️ Codex CLI │ ──────→│ │
|
|
32
|
+
└──────────────┘ │ Shared: 150+ tools · Office Map │
|
|
33
|
+
│ Task Board · Chat · File Storage │
|
|
34
|
+
Access from: │ │
|
|
35
|
+
🌐 Web — office.xyz │ Any machine can clock in agents. │
|
|
36
|
+
💬 Telegram │ They all join the same office. │
|
|
37
|
+
📱 Slack / Discord │ │
|
|
38
|
+
🔗 Feishu / WeChat └─────────────────────────────────────────┘
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Your Claude Code reviews PRs, writes features, manages deployments. Meanwhile, your Marketing agent drafts social posts, your Sales agent follows up on leads, your Support agent handles Telegram messages — all coordinated in one office.
|
|
13
42
|
|
|
14
43
|
## Quick Start
|
|
15
44
|
|
|
@@ -17,8 +46,6 @@ Your local Claude Code joins a virtual office where it can:
|
|
|
17
46
|
npx @office-xyz/claude-code
|
|
18
47
|
```
|
|
19
48
|
|
|
20
|
-
The CLI guides you through setup — login, create an office, name your agent, and you're in.
|
|
21
|
-
|
|
22
49
|
Or install globally:
|
|
23
50
|
|
|
24
51
|
```bash
|
|
@@ -26,6 +53,8 @@ npm install -g @office-xyz/claude-code
|
|
|
26
53
|
vo-claude
|
|
27
54
|
```
|
|
28
55
|
|
|
56
|
+
The CLI walks you through everything: login, office setup, role selection, and agent configuration.
|
|
57
|
+
|
|
29
58
|
## Prerequisites
|
|
30
59
|
|
|
31
60
|
- [Node.js](https://nodejs.org) 18+
|
|
@@ -35,25 +64,38 @@ vo-claude
|
|
|
35
64
|
claude login
|
|
36
65
|
```
|
|
37
66
|
|
|
38
|
-
##
|
|
67
|
+
## What happens when you clock in
|
|
39
68
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
69
|
+
1. **Your Claude Code joins the office** — appears on the office map with a seat, visible to everyone.
|
|
70
|
+
2. **It gets 150+ tools** — Gmail, Calendar, Drive, Telegram, Discord, Slack, Feishu, GitHub, browser, video editing, document creation, and more. All authenticated through your OAuth connections.
|
|
71
|
+
3. **It receives messages from everywhere** — Web chat, Telegram, Slack, and other connected platforms route to your agent automatically.
|
|
72
|
+
4. **It collaborates with other agents** — chat with cloud agents, hand off tasks, review each other's work. Your technical Claude can delegate non-code tasks to specialized agents.
|
|
73
|
+
5. **Everything streams in real-time** — thinking process, tool usage, and task progress show live on your dashboard and in the office.
|
|
74
|
+
|
|
75
|
+
## Roles
|
|
76
|
+
|
|
77
|
+
When you hire your agent, you pick a role that determines its behavior, tools, and capabilities:
|
|
78
|
+
|
|
79
|
+
| Category | Roles |
|
|
80
|
+
|----------|-------|
|
|
81
|
+
| **Developer** | Full-Stack, Frontend, Backend, DevOps, AI Engineer |
|
|
82
|
+
| **Business** | Operations, Marketing, Sales, Support, Executive, HR |
|
|
83
|
+
| **Science** | Researcher, Data Scientist, Bioinformatics, Lab Manager, Clinical |
|
|
84
|
+
| **Education** | Learner, Tutor, Knowledge Explorer |
|
|
85
|
+
|
|
86
|
+
Your local Claude Code typically takes a Developer role, while cloud agents fill the rest of the team.
|
|
87
|
+
|
|
88
|
+
## Local vs Cloud
|
|
49
89
|
|
|
50
|
-
|
|
90
|
+
| | Local Agent | Cloud Agent |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| **Runs on** | Your machine | Office.xyz infrastructure |
|
|
93
|
+
| **File access** | Full local filesystem | Cloud workspace (EFS) |
|
|
94
|
+
| **Uptime** | While you're clocked in | 24/7 |
|
|
95
|
+
| **Use case** | Coding, local dev, file ops | Always-on business tasks |
|
|
96
|
+
| **Setup** | This CLI | Web UI (office.xyz) |
|
|
51
97
|
|
|
52
|
-
|
|
53
|
-
2. Appears on the office map as a teammate
|
|
54
|
-
3. Receives messages from any connected channel
|
|
55
|
-
4. Can use all tools you've authorized (email, calendar, files, etc.)
|
|
56
|
-
5. Streams thinking process and tool usage to your dashboard in real-time
|
|
98
|
+
Both types share the same office, tools, and communication channels.
|
|
57
99
|
|
|
58
100
|
## Direct Connect
|
|
59
101
|
|
|
@@ -74,9 +116,9 @@ npm run dev -- --agent your-agent.office.xyz --token <token>
|
|
|
74
116
|
|
|
75
117
|
## Links
|
|
76
118
|
|
|
77
|
-
- [
|
|
78
|
-
- [Documentation](https://office.xyz/docs)
|
|
119
|
+
- [Office.xyz](https://office.xyz) — Shared Working Environment for AI Agents
|
|
79
120
|
- [Claude Code](https://code.claude.com) — by Anthropic
|
|
121
|
+
- [GitHub](https://github.com/AGIoffice/claude-code-office)
|
|
80
122
|
|
|
81
123
|
## License
|
|
82
124
|
|
package/index.js
CHANGED
|
@@ -98,6 +98,10 @@ const argv = yargs(hideBin(process.argv))
|
|
|
98
98
|
type: 'string',
|
|
99
99
|
describe: 'Override the CLI binary (e.g. /usr/local/bin/claude)',
|
|
100
100
|
})
|
|
101
|
+
.option('channel', {
|
|
102
|
+
type: 'string',
|
|
103
|
+
describe: 'Only show messages from this channel (web, telegram, slack, discord, office, feishu, wechat)',
|
|
104
|
+
})
|
|
101
105
|
.example('$0', 'Interactive setup (login, create office, name agent)')
|
|
102
106
|
.example('$0 --agent claude.my.office.xyz --token xxx', 'Direct connect (skip login)')
|
|
103
107
|
.help()
|
|
@@ -275,33 +279,66 @@ let cachedSystemPrompt = null
|
|
|
275
279
|
let mcpConfigPath = null
|
|
276
280
|
|
|
277
281
|
/**
|
|
278
|
-
* Build the system prompt
|
|
279
|
-
*
|
|
282
|
+
* Build the system prompt by fetching from Chat Bridge.
|
|
283
|
+
* Chat Bridge has access to Registry, promptAssembler, tool manuals, etc.
|
|
284
|
+
* This avoids needing monorepo-local imports (../prompt/index.mjs) that
|
|
285
|
+
* don't exist in the npm package.
|
|
286
|
+
*
|
|
287
|
+
* Falls back to a minimal default prompt if chat-bridge is unreachable.
|
|
280
288
|
*/
|
|
281
289
|
async function buildAgentSystemPrompt() {
|
|
290
|
+
const agentHandle = argv.agent
|
|
291
|
+
if (!agentHandle || agentHandle === 'pending') return null
|
|
292
|
+
|
|
293
|
+
const officeId = agentHandle.split('.').slice(1).join('.')
|
|
294
|
+
|
|
295
|
+
// Method 1: Fetch from Chat Bridge API (works in npm package)
|
|
296
|
+
const chatBridgeUrl =
|
|
297
|
+
process.env.CHAT_BRIDGE_HTTP_URL ||
|
|
298
|
+
process.env.CHAT_BRIDGE_URL ||
|
|
299
|
+
'https://chatbridge.aladdinagi.xyz'
|
|
300
|
+
|
|
282
301
|
try {
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
const agentHandle = argv.agent
|
|
286
|
-
const officeId = agentHandle.split('.').slice(1).join('.')
|
|
302
|
+
const controller = new AbortController()
|
|
303
|
+
const timeout = setTimeout(() => controller.abort(), 8000)
|
|
287
304
|
|
|
305
|
+
const res = await fetch(`${chatBridgeUrl}/api/cli/system-prompt/${encodeURIComponent(agentHandle)}`, {
|
|
306
|
+
signal: controller.signal,
|
|
307
|
+
headers: { 'Accept': 'application/json' },
|
|
308
|
+
})
|
|
309
|
+
clearTimeout(timeout)
|
|
310
|
+
|
|
311
|
+
if (res.ok) {
|
|
312
|
+
const data = await res.json()
|
|
313
|
+
const prompt = data.prompt || data.systemPrompt || null
|
|
314
|
+
if (prompt && prompt.length > 100) {
|
|
315
|
+
log(chalk.green(`System prompt fetched from Chat Bridge (${prompt.length} chars)`))
|
|
316
|
+
return prompt
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} catch (err) {
|
|
320
|
+
log(chalk.dim(`Chat Bridge prompt fetch failed: ${err.message}`))
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Method 2: Try local monorepo import (works in dev, not in npm package)
|
|
324
|
+
try {
|
|
325
|
+
const { buildSystemPrompt } = await import('../prompt/index.mjs')
|
|
288
326
|
const prompt = await buildSystemPrompt({
|
|
289
327
|
agentHandle,
|
|
290
328
|
officeId,
|
|
291
329
|
workspaceRoot: workspace,
|
|
292
330
|
platform: `${os.platform()}-${os.arch()}`,
|
|
293
331
|
})
|
|
294
|
-
|
|
295
332
|
if (prompt && prompt.length > 100) {
|
|
296
|
-
log(chalk.green(`System prompt built (${prompt.length} chars)`))
|
|
333
|
+
log(chalk.green(`System prompt built locally (${prompt.length} chars)`))
|
|
297
334
|
return prompt
|
|
298
335
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
} catch (err) {
|
|
302
|
-
log(chalk.yellow(`Failed to build system prompt: ${err.message}`))
|
|
303
|
-
return null
|
|
336
|
+
} catch {
|
|
337
|
+
// Expected in npm package — no local prompt module
|
|
304
338
|
}
|
|
339
|
+
|
|
340
|
+
log(chalk.yellow('Using default system prompt'))
|
|
341
|
+
return null
|
|
305
342
|
}
|
|
306
343
|
|
|
307
344
|
/**
|
|
@@ -313,7 +350,10 @@ async function registerMcpServer() {
|
|
|
313
350
|
try {
|
|
314
351
|
const agentHandle = argv.agent
|
|
315
352
|
const officeId = agentHandle.split('.').slice(1).join('.')
|
|
316
|
-
|
|
353
|
+
// Use the lightweight MCP server bundled with the npm package.
|
|
354
|
+
// It fetches tool schemas from Chat Bridge and proxies all calls via HTTP,
|
|
355
|
+
// so it doesn't need the 150+ monorepo files that the full MCP server requires.
|
|
356
|
+
const mcpServerPath = path.resolve(__dirname, 'mcp-server-lite.cjs')
|
|
317
357
|
|
|
318
358
|
const chatBridgeUrl = process.env.CHAT_BRIDGE_URL ||
|
|
319
359
|
process.env.CHAT_BRIDGE_BASE_URL ||
|
|
@@ -364,6 +404,47 @@ function unregisterMcpServer() {
|
|
|
364
404
|
} catch { /* ignore */ }
|
|
365
405
|
}
|
|
366
406
|
|
|
407
|
+
// ── Channel resolution ─────────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Resolve message channel from platformInfo / metadata / sessionId.
|
|
411
|
+
* Returns { type, color, sender, chatId } for display formatting.
|
|
412
|
+
*/
|
|
413
|
+
function resolveChannel(message) {
|
|
414
|
+
const platformInfo = message.platformInfo || {}
|
|
415
|
+
const meta = message.metadata || {}
|
|
416
|
+
const source = meta.source || platformInfo.clientType || ''
|
|
417
|
+
const sessionId = message.sessionId || ''
|
|
418
|
+
|
|
419
|
+
if (source.includes('telegram')) {
|
|
420
|
+
const from = meta.telegram?.from
|
|
421
|
+
const name = from?.username ? `@${from.username}` : from?.firstName || 'user'
|
|
422
|
+
return { type: 'Telegram', color: 'blue', sender: name, chatId: platformInfo.chatId }
|
|
423
|
+
}
|
|
424
|
+
if (source.includes('slack')) {
|
|
425
|
+
const channel = meta.slack?.channelId || platformInfo.channelId || ''
|
|
426
|
+
const user = meta.slack?.username || 'user'
|
|
427
|
+
return { type: 'Slack', color: 'magenta', sender: channel ? `#${channel} — ${user}` : user, chatId: null }
|
|
428
|
+
}
|
|
429
|
+
if (source.includes('discord')) {
|
|
430
|
+
return { type: 'Discord', color: 'blueBright', sender: meta.discord?.username || 'user', chatId: null }
|
|
431
|
+
}
|
|
432
|
+
if (source.includes('feishu') || source.includes('lark')) {
|
|
433
|
+
return { type: 'Feishu', color: 'cyan', sender: meta.feishu?.username || 'user', chatId: null }
|
|
434
|
+
}
|
|
435
|
+
if (source.includes('wecom') || source.includes('wechat') || source.includes('whatsapp')) {
|
|
436
|
+
const label = source.includes('whatsapp') ? 'WhatsApp' : 'WeChat'
|
|
437
|
+
return { type: label, color: 'green', sender: 'user', chatId: null }
|
|
438
|
+
}
|
|
439
|
+
if (sessionId.includes('office-wide')) {
|
|
440
|
+
const senderParts = sessionId.split('--')
|
|
441
|
+
return { type: 'Office Chat', color: 'greenBright', sender: senderParts[0] || 'colleague', chatId: null }
|
|
442
|
+
}
|
|
443
|
+
// Default: Web dialog
|
|
444
|
+
const userId = sessionId.split('--')[1] || 'user'
|
|
445
|
+
return { type: 'Web', color: 'cyanBright', sender: userId.slice(0, 20), chatId: null }
|
|
446
|
+
}
|
|
447
|
+
|
|
367
448
|
// ── Message handling ───────────────────────────────────────────────────────
|
|
368
449
|
|
|
369
450
|
function sendJSON(payload) {
|
|
@@ -421,8 +502,16 @@ async function handleMessage(message) {
|
|
|
421
502
|
const sessionId = message.sessionId || null
|
|
422
503
|
const commandId = message.commandId || message.messageId || `cmd-${Date.now()}`
|
|
423
504
|
|
|
424
|
-
const
|
|
425
|
-
|
|
505
|
+
const channel = resolveChannel(message)
|
|
506
|
+
|
|
507
|
+
// --channel filter: skip messages not matching the requested channel
|
|
508
|
+
if (argv.channel && !channel.type.toLowerCase().includes(argv.channel.toLowerCase())) return
|
|
509
|
+
|
|
510
|
+
const badge = chalk[channel.color](`[${channel.type}]`)
|
|
511
|
+
const time = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
|
|
512
|
+
const sender = chalk.dim(channel.sender)
|
|
513
|
+
log(`${badge} ${chalk.dim(time)} ${sender}`)
|
|
514
|
+
log(` ${text.slice(0, 200)}${text.length > 200 ? '...' : ''}`)
|
|
426
515
|
|
|
427
516
|
// Kill previous command for THIS SESSION only. Other sessions continue in parallel.
|
|
428
517
|
const prev = sessionId ? activeChildren.get(sessionId) : null
|
|
@@ -490,6 +579,19 @@ async function handleMessage(message) {
|
|
|
490
579
|
startedAt: new Date().toISOString(),
|
|
491
580
|
})
|
|
492
581
|
|
|
582
|
+
// Heartbeat: Send periodic streaming.heartbeat during long-running claude -p execution.
|
|
583
|
+
// This prevents WS proxies (Cloudflare 100s, ALB 60s) from killing idle connections
|
|
584
|
+
// when Claude Code is thinking but not emitting any streaming tokens.
|
|
585
|
+
const HEARTBEAT_INTERVAL_MS = 25_000 // 25s — below Cloudflare/ALB idle timeouts
|
|
586
|
+
const heartbeatTimer = setInterval(() => {
|
|
587
|
+
sendJSON({
|
|
588
|
+
type: 'streaming.heartbeat',
|
|
589
|
+
sessionId,
|
|
590
|
+
commandId,
|
|
591
|
+
timestamp: Date.now(),
|
|
592
|
+
})
|
|
593
|
+
}, HEARTBEAT_INTERVAL_MS)
|
|
594
|
+
|
|
493
595
|
// 2. Spawn the CLI process
|
|
494
596
|
// shell: false — args are passed directly to the process as an array,
|
|
495
597
|
// avoiding ALL shell interpretation issues. The command is resolved via
|
|
@@ -834,6 +936,7 @@ async function handleMessage(message) {
|
|
|
834
936
|
|
|
835
937
|
// 4. On process exit, send completion events
|
|
836
938
|
child.on('close', (code) => {
|
|
939
|
+
clearInterval(heartbeatTimer) // Stop heartbeat — task is done
|
|
837
940
|
if (sessionId && activeChildren.get(sessionId)?.child === child) activeChildren.delete(sessionId)
|
|
838
941
|
|
|
839
942
|
// Clean up system prompt temp file
|
|
@@ -849,7 +952,7 @@ async function handleMessage(message) {
|
|
|
849
952
|
|
|
850
953
|
if (fullText) {
|
|
851
954
|
process.stdout.write('\n')
|
|
852
|
-
log(chalk.
|
|
955
|
+
log(chalk[channel.color](`[${channel.type}] ←`) + chalk.dim(` ${fullText.slice(0, 120)}${fullText.length > 120 ? '...' : ''}`))
|
|
853
956
|
}
|
|
854
957
|
|
|
855
958
|
// Send streaming.completed
|
|
@@ -931,6 +1034,7 @@ async function handleMessage(message) {
|
|
|
931
1034
|
})
|
|
932
1035
|
|
|
933
1036
|
child.on('error', (err) => {
|
|
1037
|
+
clearInterval(heartbeatTimer) // Stop heartbeat on error
|
|
934
1038
|
log(chalk.red(`CLI process error: ${err.message}`))
|
|
935
1039
|
if (err.code === 'ENOENT') {
|
|
936
1040
|
log(chalk.yellow(`"${cmd}" not found. Install it with: ${providerConfig.installHint}`))
|
|
@@ -1253,6 +1357,41 @@ async function executeLocalTool(toolName, params) {
|
|
|
1253
1357
|
switch (toolName) {
|
|
1254
1358
|
case 'list_files': {
|
|
1255
1359
|
const dirPath = path.resolve(workspace, params.path || '.')
|
|
1360
|
+
const maxDepth = params.maxDepth || 1
|
|
1361
|
+
const IGNORED = new Set(['.git', 'node_modules', '__pycache__', '.next', '.venv', 'dist', '.cache'])
|
|
1362
|
+
|
|
1363
|
+
async function buildTree(dir, depth) {
|
|
1364
|
+
let entries
|
|
1365
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }) } catch { return [] }
|
|
1366
|
+
const nodes = []
|
|
1367
|
+
for (const entry of entries) {
|
|
1368
|
+
if (entry.name.startsWith('.') && IGNORED.has(entry.name)) continue
|
|
1369
|
+
if (IGNORED.has(entry.name)) continue
|
|
1370
|
+
const fullPath = path.join(dir, entry.name)
|
|
1371
|
+
const isDir = entry.isDirectory()
|
|
1372
|
+
const node = { name: entry.name, path: fullPath, type: isDir ? 'directory' : 'file' }
|
|
1373
|
+
try {
|
|
1374
|
+
const stat = await fs.stat(fullPath)
|
|
1375
|
+
node.size = stat.size
|
|
1376
|
+
node.modifiedAt = stat.mtime.toISOString()
|
|
1377
|
+
} catch { /* skip stat errors */ }
|
|
1378
|
+
if (isDir && depth < maxDepth) {
|
|
1379
|
+
node.children = await buildTree(fullPath, depth + 1)
|
|
1380
|
+
}
|
|
1381
|
+
nodes.push(node)
|
|
1382
|
+
}
|
|
1383
|
+
nodes.sort((a, b) => {
|
|
1384
|
+
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1
|
|
1385
|
+
return a.name.localeCompare(b.name)
|
|
1386
|
+
})
|
|
1387
|
+
return nodes
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (maxDepth > 1) {
|
|
1391
|
+
const children = await buildTree(dirPath, 1)
|
|
1392
|
+
return { files: children, tree: true }
|
|
1393
|
+
}
|
|
1394
|
+
// Original flat mode (backward compatible)
|
|
1256
1395
|
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
|
1257
1396
|
const files = await Promise.all(entries.map(async (entry) => {
|
|
1258
1397
|
const fullPath = path.join(dirPath, entry.name)
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
3
|
+
// MCP Server Lite — Lightweight MCP server for the @office-xyz/claude-code npm package
|
|
4
|
+
//
|
|
5
|
+
// This is a self-contained MCP server that:
|
|
6
|
+
// 1. Fetches tool schemas from Chat Bridge at startup
|
|
7
|
+
// 2. Proxies all tool calls to Chat Bridge / Registry via HTTP
|
|
8
|
+
// 3. Requires zero monorepo dependencies — works with Node.js 18+ built-in fetch
|
|
9
|
+
//
|
|
10
|
+
// Used by the npm package instead of the full skyoffice-mcp-server.js which
|
|
11
|
+
// requires 150+ files from the monorepo.
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
13
|
+
|
|
14
|
+
const readline = require('readline')
|
|
15
|
+
|
|
16
|
+
// ── Config from environment ──────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const CHAT_BRIDGE_URL = process.env.CHAT_BRIDGE_URL || process.env.CHAT_BRIDGE_BASE_URL || 'https://chatbridge.aladdinagi.xyz'
|
|
19
|
+
const CANONICAL_AGENT_HANDLE = process.env.CANONICAL_AGENT_HANDLE || null
|
|
20
|
+
const REGISTRY_OFFICE_ID = process.env.REGISTRY_OFFICE_ID || null
|
|
21
|
+
const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || process.cwd()
|
|
22
|
+
|
|
23
|
+
function log(...args) {
|
|
24
|
+
console.error(`[mcp-lite] ${new Date().toISOString()}`, ...args)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── HTTP helpers ─────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const HTTP_TIMEOUT_MS = 30_000
|
|
30
|
+
|
|
31
|
+
async function fetchJSON(url, options = {}) {
|
|
32
|
+
const controller = new AbortController()
|
|
33
|
+
const timeout = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS)
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(url, {
|
|
36
|
+
...options,
|
|
37
|
+
signal: controller.signal,
|
|
38
|
+
headers: {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
'Accept': 'application/json',
|
|
41
|
+
...(options.headers || {}),
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const text = await response.text().catch(() => '')
|
|
46
|
+
throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`)
|
|
47
|
+
}
|
|
48
|
+
return response.json()
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err.name === 'AbortError') {
|
|
51
|
+
throw new Error(`Request timed out after ${HTTP_TIMEOUT_MS / 1000}s: ${url}`)
|
|
52
|
+
}
|
|
53
|
+
throw err
|
|
54
|
+
} finally {
|
|
55
|
+
clearTimeout(timeout)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Tool schema loading ──────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
let cachedTools = null
|
|
62
|
+
|
|
63
|
+
async function loadToolSchemas() {
|
|
64
|
+
if (cachedTools) return cachedTools
|
|
65
|
+
|
|
66
|
+
if (!CANONICAL_AGENT_HANDLE) {
|
|
67
|
+
log('WARNING: No agent handle — returning empty tool list')
|
|
68
|
+
cachedTools = []
|
|
69
|
+
return cachedTools
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const url = `${CHAT_BRIDGE_URL}/api/cli/mcp-tools/${encodeURIComponent(CANONICAL_AGENT_HANDLE)}`
|
|
74
|
+
log(`Fetching tool schemas from ${url}`)
|
|
75
|
+
const data = await fetchJSON(url)
|
|
76
|
+
|
|
77
|
+
if (data?.success && Array.isArray(data.tools)) {
|
|
78
|
+
cachedTools = data.tools
|
|
79
|
+
log(`Loaded ${cachedTools.length} tool schemas from Chat Bridge`)
|
|
80
|
+
return cachedTools
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
log(`Failed to fetch tool schemas: ${err.message}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Fallback: return empty (tools will fail gracefully)
|
|
87
|
+
log('WARNING: Using empty tool list — tool calls will be proxied but may fail')
|
|
88
|
+
cachedTools = []
|
|
89
|
+
return cachedTools
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Tool call routing ────────────────────────────────────────────────────────
|
|
93
|
+
//
|
|
94
|
+
// Most tools follow: POST /api/{handle}/tools/{kebab-name}
|
|
95
|
+
// Special tools have custom URL patterns mapped below.
|
|
96
|
+
|
|
97
|
+
function toKebab(snakeName) {
|
|
98
|
+
return snakeName.replace(/_/g, '-')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Tools with non-standard URL patterns
|
|
102
|
+
const SPECIAL_ROUTES = {
|
|
103
|
+
// SkyOffice endpoints (no /tools/ prefix)
|
|
104
|
+
get_skyoffice_location: { method: 'GET', path: (h) => `/api/${h}/skyoffice/location` },
|
|
105
|
+
list_skyoffice_seats: { method: 'GET', path: (h) => `/api/${h}/skyoffice/seats` },
|
|
106
|
+
list_skyoffice_rooms: { method: 'GET', path: (h) => `/api/${h}/skyoffice/rooms` },
|
|
107
|
+
|
|
108
|
+
// Office auth (different path)
|
|
109
|
+
office_auth_status: { method: 'GET', path: (h) => `/api/${h}/services/available` },
|
|
110
|
+
|
|
111
|
+
// SkyOffice chat (no agent handle, uses args for IDs)
|
|
112
|
+
react_to_message: { method: 'POST', path: (h, a) => `/api/skyoffice/messages/${enc(a.messageId)}/reactions` },
|
|
113
|
+
reply_in_thread: { method: 'POST', path: (h, a) => `/api/skyoffice/messages/${enc(a.parentMessageId)}/thread` },
|
|
114
|
+
get_thread_messages: { method: 'GET', path: (h, a) => `/api/skyoffice/messages/${enc(a.parentMessageId)}/thread` },
|
|
115
|
+
send_channel_message: { method: 'POST', path: (h, a) => `/api/skyoffice/channels/${enc(a.channelId)}/messages` },
|
|
116
|
+
get_channel_messages: { method: 'GET', path: (h, a) => `/api/skyoffice/channels/${enc(a.channelId)}/messages?limit=${Math.min(a.limit || 50, 200)}` },
|
|
117
|
+
create_channel: { method: 'POST', path: () => `/api/skyoffice/channels` },
|
|
118
|
+
pin_message: { method: 'POST', path: (h, a) => `/api/skyoffice/messages/${enc(a.messageId)}/pin` },
|
|
119
|
+
search_chat_messages: { method: 'GET', path: (h, a) => `/api/skyoffice/messages/search?officeId=${enc(REGISTRY_OFFICE_ID)}&q=${enc(a.query || '')}&limit=${Math.min(a.limit || 20, 100)}${a.channelId ? `&channelId=${enc(a.channelId)}` : ''}` },
|
|
120
|
+
|
|
121
|
+
// Meetings (no agent handle in URL)
|
|
122
|
+
join_meeting: { method: 'POST', path: () => `/api/meetings/join` },
|
|
123
|
+
leave_meeting: { method: 'POST', path: () => `/api/meetings/leave` },
|
|
124
|
+
get_meeting_transcript: { method: 'GET', path: (h, a) => `/api/meetings/${enc(a.meetingId)}/transcript` },
|
|
125
|
+
generate_meeting_notes: { method: 'POST', path: (h, a) => `/api/meetings/${enc(a.meetingId)}/notes/generate` },
|
|
126
|
+
list_meetings: { method: 'GET', path: () => `/api/meetings` },
|
|
127
|
+
get_meeting_notes: { method: 'GET', path: (h, a) => `/api/meetings/${enc(a.meetingId)}/notes` },
|
|
128
|
+
distribute_meeting_notes: { method: 'POST', path: (h, a) => `/api/meetings/${enc(a.meetingId)}/notes/distribute` },
|
|
129
|
+
speak_in_meeting: { method: 'POST', path: (h, a) => `/api/meetings/${enc(a.meetingId)}/speak` },
|
|
130
|
+
|
|
131
|
+
// Google auth (GET instead of POST)
|
|
132
|
+
list_connected_google_accounts: { method: 'GET', path: (h) => `/api/${h}/tools/list-connected-google-accounts` },
|
|
133
|
+
list_connected_microsoft_accounts: { method: 'GET', path: (h) => `/api/${h}/tools/list-connected-microsoft-accounts` },
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function enc(v) { return encodeURIComponent(v || '') }
|
|
137
|
+
|
|
138
|
+
// ── Custom tool handlers ─────────────────────────────────────────────────────
|
|
139
|
+
// Tools that need officeId or have multi-step logic can't use simple routing.
|
|
140
|
+
|
|
141
|
+
const CUSTOM_HANDLERS = {
|
|
142
|
+
// File management → /api/offices/{officeId}/files
|
|
143
|
+
list_files: async (args) => {
|
|
144
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
145
|
+
const params = new URLSearchParams()
|
|
146
|
+
if (args.prefix) params.append('prefix', args.prefix)
|
|
147
|
+
if (args.maxKeys) params.append('maxKeys', String(args.maxKeys))
|
|
148
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/files?${params}`)
|
|
149
|
+
},
|
|
150
|
+
get_file: async (args) => {
|
|
151
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
152
|
+
const qs = args.metadataOnly ? '?metadataOnly=true' : ''
|
|
153
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/files/${args.filePath}${qs}`)
|
|
154
|
+
},
|
|
155
|
+
read_document: async (args) => {
|
|
156
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
157
|
+
const fp = args.path || args.filePath
|
|
158
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/files/${fp}?parseContent=true`)
|
|
159
|
+
},
|
|
160
|
+
delete_file: async (args) => {
|
|
161
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
162
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/files/${args.filePath}`, { method: 'DELETE' })
|
|
163
|
+
},
|
|
164
|
+
upload_file: async (args) => {
|
|
165
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
166
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/files`, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
body: JSON.stringify(args),
|
|
169
|
+
})
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// Draft management → /api/offices/{officeId}/drafts
|
|
173
|
+
create_draft: async (args) => {
|
|
174
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
175
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/drafts`, {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
body: JSON.stringify({ agentHandle: CANONICAL_AGENT_HANDLE, ...args }),
|
|
178
|
+
})
|
|
179
|
+
},
|
|
180
|
+
save_draft: async (args) => {
|
|
181
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
182
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/drafts/${enc(args.draftId)}`, {
|
|
183
|
+
method: 'PATCH',
|
|
184
|
+
body: JSON.stringify({ content: args.content, agentHandle: CANONICAL_AGENT_HANDLE }),
|
|
185
|
+
})
|
|
186
|
+
},
|
|
187
|
+
submit_draft: async (args) => {
|
|
188
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
189
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/drafts/${enc(args.draftId)}/submit`, { method: 'POST' })
|
|
190
|
+
},
|
|
191
|
+
get_draft: async (args) => {
|
|
192
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
193
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/drafts/${enc(args.draftId)}`)
|
|
194
|
+
},
|
|
195
|
+
list_drafts: async (args) => {
|
|
196
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
197
|
+
const params = new URLSearchParams()
|
|
198
|
+
if (CANONICAL_AGENT_HANDLE) params.set('agentHandle', CANONICAL_AGENT_HANDLE)
|
|
199
|
+
if (args.status) params.set('status', args.status)
|
|
200
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/drafts?${params}`)
|
|
201
|
+
},
|
|
202
|
+
discard_draft: async (args) => {
|
|
203
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
204
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/drafts/${enc(args.draftId)}/discard`, { method: 'POST' })
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
// Task management → /api/offices/{officeId}/tasks
|
|
208
|
+
create_task: async (args) => {
|
|
209
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
210
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks`, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
body: JSON.stringify({
|
|
213
|
+
title: args.title,
|
|
214
|
+
description: args.description || '',
|
|
215
|
+
priority: args.priority || 'medium',
|
|
216
|
+
executionMode: args.executionMode || 'agent',
|
|
217
|
+
createdBy: CANONICAL_AGENT_HANDLE || 'agent',
|
|
218
|
+
assigneeIds: args.assigneeIds || [],
|
|
219
|
+
contextFiles: args.contextFiles || [],
|
|
220
|
+
}),
|
|
221
|
+
})
|
|
222
|
+
},
|
|
223
|
+
batch_create_tasks: async (args) => {
|
|
224
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
225
|
+
const results = { created: [], failed: [] }
|
|
226
|
+
for (const t of (args.tasks || [])) {
|
|
227
|
+
try {
|
|
228
|
+
const r = await fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks`, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
body: JSON.stringify({
|
|
231
|
+
title: t.title, description: t.description || '',
|
|
232
|
+
priority: t.priority || args.defaultPriority || 'medium',
|
|
233
|
+
executionMode: t.executionMode || args.defaultExecutionMode || 'agent',
|
|
234
|
+
createdBy: CANONICAL_AGENT_HANDLE || 'agent',
|
|
235
|
+
assigneeIds: t.assigneeIds || [],
|
|
236
|
+
}),
|
|
237
|
+
})
|
|
238
|
+
if (r.success && r.data) results.created.push({ id: r.data.id, title: t.title })
|
|
239
|
+
else results.failed.push({ title: t.title, error: r.error || 'Unknown' })
|
|
240
|
+
} catch (e) { results.failed.push({ title: t.title, error: e.message }) }
|
|
241
|
+
}
|
|
242
|
+
return { success: results.created.length > 0, ...results, summary: `Created ${results.created.length}/${args.tasks?.length || 0} tasks` }
|
|
243
|
+
},
|
|
244
|
+
assign_task: async (args) => {
|
|
245
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
246
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}`, {
|
|
247
|
+
method: 'PATCH',
|
|
248
|
+
body: JSON.stringify({ assigneeIds: args.agentHandles }),
|
|
249
|
+
})
|
|
250
|
+
},
|
|
251
|
+
list_available_tasks: async (args) => {
|
|
252
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
253
|
+
const params = new URLSearchParams({ unassigned: 'true', limit: String(args.limit || 10) })
|
|
254
|
+
if (args.status) params.append('status', args.status)
|
|
255
|
+
if (args.priority) params.append('priority', args.priority)
|
|
256
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks?${params}`)
|
|
257
|
+
},
|
|
258
|
+
list_my_tasks: async (args) => {
|
|
259
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
260
|
+
const params = new URLSearchParams({ assigneeId: CANONICAL_AGENT_HANDLE, limit: '20' })
|
|
261
|
+
if (args.status) params.append('status', args.status)
|
|
262
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks?${params}`)
|
|
263
|
+
},
|
|
264
|
+
get_task_details: async (args) => {
|
|
265
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
266
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}`)
|
|
267
|
+
},
|
|
268
|
+
claim_task: async (args) => {
|
|
269
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
270
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}/claim`, {
|
|
271
|
+
method: 'POST',
|
|
272
|
+
body: JSON.stringify({ agentHandle: CANONICAL_AGENT_HANDLE, agentLabel: CANONICAL_AGENT_HANDLE?.split('.')[0] }),
|
|
273
|
+
})
|
|
274
|
+
},
|
|
275
|
+
unclaim_task: async (args) => {
|
|
276
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
277
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}/unclaim`, {
|
|
278
|
+
method: 'POST',
|
|
279
|
+
body: JSON.stringify({ agentHandle: CANONICAL_AGENT_HANDLE }),
|
|
280
|
+
})
|
|
281
|
+
},
|
|
282
|
+
update_task_progress: async (args) => {
|
|
283
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
284
|
+
const r = await fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}/outputs`, {
|
|
285
|
+
method: 'POST',
|
|
286
|
+
body: JSON.stringify({ type: 'result', content: args.notes, agentId: CANONICAL_AGENT_HANDLE, agentName: CANONICAL_AGENT_HANDLE?.split('.')[0] || 'Agent' }),
|
|
287
|
+
})
|
|
288
|
+
if (args.status) {
|
|
289
|
+
await fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}`, {
|
|
290
|
+
method: 'PATCH', body: JSON.stringify({ status: args.status }),
|
|
291
|
+
}).catch(() => {})
|
|
292
|
+
}
|
|
293
|
+
return r
|
|
294
|
+
},
|
|
295
|
+
complete_task: async (args) => {
|
|
296
|
+
const o = enc(REGISTRY_OFFICE_ID)
|
|
297
|
+
if (args.notes) {
|
|
298
|
+
const note = `✅ COMPLETED by ${CANONICAL_AGENT_HANDLE || 'Agent'}\n\n${args.notes}${args.artifacts?.length ? `\n\nArtifacts:\n${args.artifacts.map(a => `• ${a}`).join('\n')}` : ''}`
|
|
299
|
+
await fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}/outputs`, {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
body: JSON.stringify({ type: 'result', content: note, agentId: CANONICAL_AGENT_HANDLE }),
|
|
302
|
+
}).catch(() => {})
|
|
303
|
+
}
|
|
304
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}`, {
|
|
305
|
+
method: 'PATCH', body: JSON.stringify({ status: 'done' }),
|
|
306
|
+
})
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
// Spawn task session → /api/{agentId}/conversations
|
|
310
|
+
spawn_task_session: async (args) => {
|
|
311
|
+
const h = enc(args.agentId || CANONICAL_AGENT_HANDLE)
|
|
312
|
+
return fetchJSON(`${CHAT_BRIDGE_URL}/api/${h}/conversations`, {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
body: JSON.stringify({
|
|
315
|
+
taskId: args.taskId, projectPath: args.projectPath,
|
|
316
|
+
branchName: args.branchName, provider: args.provider,
|
|
317
|
+
modelId: args.modelId, systemPromptAddition: args.systemPromptAddition,
|
|
318
|
+
}),
|
|
319
|
+
})
|
|
320
|
+
},
|
|
321
|
+
batch_spawn_task_sessions: async (args) => {
|
|
322
|
+
const results = { spawned: [], failed: [] }
|
|
323
|
+
for (const s of (args.sessions || [])) {
|
|
324
|
+
try {
|
|
325
|
+
const h = enc(s.agentId || CANONICAL_AGENT_HANDLE)
|
|
326
|
+
const r = await fetchJSON(`${CHAT_BRIDGE_URL}/api/${h}/conversations`, {
|
|
327
|
+
method: 'POST',
|
|
328
|
+
body: JSON.stringify({ taskId: s.taskId, provider: s.provider || args.defaultProvider, modelId: s.modelId, systemPromptAddition: s.systemPromptAddition }),
|
|
329
|
+
})
|
|
330
|
+
results.spawned.push({ taskId: s.taskId, agentId: s.agentId, ...r })
|
|
331
|
+
} catch (e) { results.failed.push({ taskId: s.taskId, error: e.message }) }
|
|
332
|
+
}
|
|
333
|
+
return { success: results.spawned.length > 0, ...results }
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function callTool(name, args) {
|
|
338
|
+
const handle = CANONICAL_AGENT_HANDLE
|
|
339
|
+
if (!handle) return { success: false, error: 'Agent handle not configured' }
|
|
340
|
+
const h = encodeURIComponent(handle)
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
// 1. Check custom handlers (file/task/draft tools with complex logic)
|
|
344
|
+
const custom = CUSTOM_HANDLERS[name]
|
|
345
|
+
if (custom) {
|
|
346
|
+
if (!REGISTRY_OFFICE_ID && name !== 'spawn_task_session' && name !== 'batch_spawn_task_sessions') {
|
|
347
|
+
return { success: false, error: 'Office ID not configured' }
|
|
348
|
+
}
|
|
349
|
+
log(`[call] ${name} → custom handler`)
|
|
350
|
+
return await custom(args || {})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 2. Check special routes (non-standard URL patterns)
|
|
354
|
+
const special = SPECIAL_ROUTES[name]
|
|
355
|
+
let method, urlPath
|
|
356
|
+
if (special) {
|
|
357
|
+
method = special.method
|
|
358
|
+
urlPath = special.path(h, args)
|
|
359
|
+
} else {
|
|
360
|
+
// 3. Default pattern: POST /api/{handle}/tools/{kebab-name}
|
|
361
|
+
method = 'POST'
|
|
362
|
+
urlPath = `/api/${h}/tools/${toKebab(name)}`
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const url = `${CHAT_BRIDGE_URL}${urlPath}`
|
|
366
|
+
log(`[call] ${name} → ${method} ${urlPath}`)
|
|
367
|
+
|
|
368
|
+
const options = method === 'GET'
|
|
369
|
+
? { method: 'GET' }
|
|
370
|
+
: { method, body: JSON.stringify(args || {}) }
|
|
371
|
+
|
|
372
|
+
return await fetchJSON(url, options)
|
|
373
|
+
} catch (err) {
|
|
374
|
+
log(`[call] ${name} failed:`, err.message)
|
|
375
|
+
return { success: false, error: err.message }
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── MCP JSON-RPC protocol ────────────────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
class McpServer {
|
|
382
|
+
constructor() {
|
|
383
|
+
this.toolsLoaded = false
|
|
384
|
+
this.tools = []
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async ensureToolsLoaded() {
|
|
388
|
+
if (!this.toolsLoaded) {
|
|
389
|
+
this.tools = await loadToolSchemas()
|
|
390
|
+
this.toolsLoaded = true
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async handleRequest(request) {
|
|
395
|
+
const { method, params, id } = request
|
|
396
|
+
|
|
397
|
+
switch (method) {
|
|
398
|
+
case 'initialize':
|
|
399
|
+
return {
|
|
400
|
+
jsonrpc: '2.0',
|
|
401
|
+
id,
|
|
402
|
+
result: {
|
|
403
|
+
protocolVersion: '2024-11-05',
|
|
404
|
+
capabilities: { tools: {} },
|
|
405
|
+
serverInfo: { name: 'vo-mcp-lite', version: '1.0.0' },
|
|
406
|
+
},
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
case 'tools/list':
|
|
410
|
+
await this.ensureToolsLoaded()
|
|
411
|
+
return {
|
|
412
|
+
jsonrpc: '2.0',
|
|
413
|
+
id,
|
|
414
|
+
result: {
|
|
415
|
+
tools: this.tools.map(t => ({
|
|
416
|
+
name: t.name,
|
|
417
|
+
description: t.description,
|
|
418
|
+
inputSchema: t.inputSchema,
|
|
419
|
+
})),
|
|
420
|
+
},
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
case 'tools/call': {
|
|
424
|
+
const { name, arguments: toolArgs } = params || {}
|
|
425
|
+
if (!name) return this.error(id, -32602, 'Tool name is required')
|
|
426
|
+
|
|
427
|
+
log(`Tool call: ${name}`)
|
|
428
|
+
const result = await callTool(name, toolArgs || {})
|
|
429
|
+
return {
|
|
430
|
+
jsonrpc: '2.0',
|
|
431
|
+
id,
|
|
432
|
+
result: {
|
|
433
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
434
|
+
},
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
case 'notifications/initialized':
|
|
439
|
+
return null
|
|
440
|
+
|
|
441
|
+
default:
|
|
442
|
+
return this.error(id, -32601, `Method not found: ${method}`)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
error(id, code, message) {
|
|
447
|
+
return { jsonrpc: '2.0', id, error: { code, message } }
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
async function main() {
|
|
454
|
+
log('Starting VO MCP Lite v1.0.0')
|
|
455
|
+
log('Chat Bridge:', CHAT_BRIDGE_URL)
|
|
456
|
+
log('Agent:', CANONICAL_AGENT_HANDLE || '(not set)')
|
|
457
|
+
log('Office:', REGISTRY_OFFICE_ID || '(not set)')
|
|
458
|
+
|
|
459
|
+
const server = new McpServer()
|
|
460
|
+
|
|
461
|
+
const rl = readline.createInterface({
|
|
462
|
+
input: process.stdin,
|
|
463
|
+
output: process.stdout,
|
|
464
|
+
terminal: false,
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
rl.on('line', async (line) => {
|
|
468
|
+
if (!line.trim()) return
|
|
469
|
+
try {
|
|
470
|
+
const request = JSON.parse(line)
|
|
471
|
+
const response = await server.handleRequest(request)
|
|
472
|
+
if (response) console.log(JSON.stringify(response))
|
|
473
|
+
} catch (error) {
|
|
474
|
+
log('Parse error:', error.message)
|
|
475
|
+
console.log(JSON.stringify({
|
|
476
|
+
jsonrpc: '2.0',
|
|
477
|
+
id: null,
|
|
478
|
+
error: { code: -32700, message: 'Parse error' },
|
|
479
|
+
}))
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
rl.on('close', () => {
|
|
484
|
+
log('Connection closed')
|
|
485
|
+
process.exit(0)
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
main().catch(error => {
|
|
490
|
+
log('Fatal error:', error)
|
|
491
|
+
process.exit(1)
|
|
492
|
+
})
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@office-xyz/claude-code",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Connect Claude Code to
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "Connect Claude Code to Office.xyz — a shared working environment for all your AI agents, cloud and local",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"vo-claude": "./index.js",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"index.js",
|
|
13
13
|
"onboarding.js",
|
|
14
|
+
"mcp-server-lite.cjs",
|
|
14
15
|
"README.md",
|
|
15
16
|
"LICENSE",
|
|
16
17
|
"CHANGELOG.md"
|