-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmcp-server.js
More file actions
144 lines (127 loc) · 4.85 KB
/
mcp-server.js
File metadata and controls
144 lines (127 loc) · 4.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
const { z } = require('zod');
const http = require('http');
const path = require('path');
const fs = require('fs');
// Load .env from centralized config at ~/.cc-mob/ so server and plugin share the same token
const os = require('os');
const envPath = path.join(os.homedir(), '.cc-mob', '.env');
try {
const content = fs.readFileSync(envPath, 'utf8');
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq === -1) continue;
const key = trimmed.slice(0, eq);
const val = trimmed.slice(eq + 1);
if (!process.env[key]) process.env[key] = val;
}
} catch (e) {}
const PORT = process.env.PORT || 3456;
const TOKEN = process.env.AUTH_TOKEN || '';
function httpRequest(method, urlPath, body, timeoutMs = 300000) {
return new Promise((resolve, reject) => {
const options = {
hostname: '127.0.0.1',
port: PORT,
path: `${urlPath}${urlPath.includes('?') ? '&' : '?'}token=${TOKEN}`,
method,
headers: { 'Content-Type': 'application/json' },
timeout: timeoutMs,
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch {
resolve(data);
}
});
});
req.on('error', reject);
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
if (body) req.write(JSON.stringify(body));
req.end();
});
}
const mcpServer = new McpServer({
name: 'cc-mob',
version: '1.0.0',
});
mcpServer.tool(
'ask_user',
`Ask the user a question via their phone. Use this tool with the SAME parameters you would use for AskUserQuestion.
Parameters:
- questions: Array of 1-4 question objects, each with:
- question (string): The question to ask
- header (string): Short label displayed as a chip/tag (max 12 chars)
- options (array): Available choices, each with label (string) and description (string)
- multiSelect (boolean): Whether multiple options can be selected
The user will always have an "Other" free-text option automatically. Returns an answers object mapping question text to the selected label or custom text.`,
{
questions: z.array(z.object({
question: z.string(),
header: z.string(),
options: z.array(z.object({
label: z.string(),
description: z.string(),
})).min(2).max(4),
multiSelect: z.boolean(),
})).min(1).max(4).optional()
.describe('Array of 1-4 questions with options. Use this format for rich question cards.'),
question: z.string().optional()
.describe('Simple question string (legacy format). Prefer using "questions" array instead.'),
options: z.array(z.string()).optional()
.describe('Simple options list (legacy format). Prefer using "questions" array instead.'),
},
async (params) => {
try {
let payload;
if (params.questions && params.questions.length > 0) {
// New rich format
payload = { questions: params.questions };
} else if (params.question) {
// Legacy simple format - pass through as-is for backwards compat
payload = { question: params.question, options: params.options || [] };
} else {
return { content: [{ type: 'text', text: 'Error: Must provide either "questions" array or "question" string.' }] };
}
// Create request on server
const createRes = await httpRequest('POST', '/api/request', {
type: 'question',
payload,
});
if (!createRes.id) {
return { content: [{ type: 'text', text: 'Error: Failed to create question request. Is the cc-mob server running?' }] };
}
// Long-poll for answer
const waitRes = await httpRequest('GET', `/api/request/${createRes.id}/wait`);
if (waitRes.response && waitRes.response.answer) {
const answer = waitRes.response.answer;
// If answer is an object (multi-question answers map), return as JSON
if (typeof answer === 'object') {
return { content: [{ type: 'text', text: JSON.stringify({ answers: answer }) }] };
}
return { content: [{ type: 'text', text: answer }] };
}
return { content: [{ type: 'text', text: 'No response from user (timeout)' }] };
} catch (err) {
return { content: [{ type: 'text', text: `Error reaching cc-mob server: ${err.message}` }] };
}
}
);
async function main() {
const transport = new StdioServerTransport();
await mcpServer.connect(transport);
}
main().catch((err) => {
process.stderr.write(`MCP server error: ${err.message}\n`);
process.exit(1);
});