@office-xyz/claude-code 0.1.7 → 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/index.js +75 -4
- package/mcp-server-lite.cjs +492 -0
- package/package.json +2 -1
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()
|
|
@@ -346,7 +350,10 @@ async function registerMcpServer() {
|
|
|
346
350
|
try {
|
|
347
351
|
const agentHandle = argv.agent
|
|
348
352
|
const officeId = agentHandle.split('.').slice(1).join('.')
|
|
349
|
-
|
|
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')
|
|
350
357
|
|
|
351
358
|
const chatBridgeUrl = process.env.CHAT_BRIDGE_URL ||
|
|
352
359
|
process.env.CHAT_BRIDGE_BASE_URL ||
|
|
@@ -397,6 +404,47 @@ function unregisterMcpServer() {
|
|
|
397
404
|
} catch { /* ignore */ }
|
|
398
405
|
}
|
|
399
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
|
+
|
|
400
448
|
// ── Message handling ───────────────────────────────────────────────────────
|
|
401
449
|
|
|
402
450
|
function sendJSON(payload) {
|
|
@@ -454,8 +502,16 @@ async function handleMessage(message) {
|
|
|
454
502
|
const sessionId = message.sessionId || null
|
|
455
503
|
const commandId = message.commandId || message.messageId || `cmd-${Date.now()}`
|
|
456
504
|
|
|
457
|
-
const
|
|
458
|
-
|
|
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 ? '...' : ''}`)
|
|
459
515
|
|
|
460
516
|
// Kill previous command for THIS SESSION only. Other sessions continue in parallel.
|
|
461
517
|
const prev = sessionId ? activeChildren.get(sessionId) : null
|
|
@@ -523,6 +579,19 @@ async function handleMessage(message) {
|
|
|
523
579
|
startedAt: new Date().toISOString(),
|
|
524
580
|
})
|
|
525
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
|
+
|
|
526
595
|
// 2. Spawn the CLI process
|
|
527
596
|
// shell: false — args are passed directly to the process as an array,
|
|
528
597
|
// avoiding ALL shell interpretation issues. The command is resolved via
|
|
@@ -867,6 +936,7 @@ async function handleMessage(message) {
|
|
|
867
936
|
|
|
868
937
|
// 4. On process exit, send completion events
|
|
869
938
|
child.on('close', (code) => {
|
|
939
|
+
clearInterval(heartbeatTimer) // Stop heartbeat — task is done
|
|
870
940
|
if (sessionId && activeChildren.get(sessionId)?.child === child) activeChildren.delete(sessionId)
|
|
871
941
|
|
|
872
942
|
// Clean up system prompt temp file
|
|
@@ -882,7 +952,7 @@ async function handleMessage(message) {
|
|
|
882
952
|
|
|
883
953
|
if (fullText) {
|
|
884
954
|
process.stdout.write('\n')
|
|
885
|
-
log(chalk.
|
|
955
|
+
log(chalk[channel.color](`[${channel.type}] ←`) + chalk.dim(` ${fullText.slice(0, 120)}${fullText.length > 120 ? '...' : ''}`))
|
|
886
956
|
}
|
|
887
957
|
|
|
888
958
|
// Send streaming.completed
|
|
@@ -964,6 +1034,7 @@ async function handleMessage(message) {
|
|
|
964
1034
|
})
|
|
965
1035
|
|
|
966
1036
|
child.on('error', (err) => {
|
|
1037
|
+
clearInterval(heartbeatTimer) // Stop heartbeat on error
|
|
967
1038
|
log(chalk.red(`CLI process error: ${err.message}`))
|
|
968
1039
|
if (err.code === 'ENOENT') {
|
|
969
1040
|
log(chalk.yellow(`"${cmd}" not found. Install it with: ${providerConfig.installHint}`))
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@office-xyz/claude-code",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
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": {
|
|
@@ -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"
|