mojulo 0.0.0 → 0.1.1
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 +54 -4
- package/lib/audit-logger-new.js +11 -0
- package/lib/auth/gate.js +25 -0
- package/lib/auth/service.js +17 -0
- package/lib/auth/session.js +63 -0
- package/lib/builder/chat-processor.js +607 -0
- package/lib/builder/composer-bridge.js +82 -0
- package/lib/builder/evaluator.js +159 -0
- package/lib/builder/executor.js +252 -0
- package/lib/builder/index.js +48 -0
- package/lib/builder/session.js +248 -0
- package/lib/builder/system-prompt.js +422 -0
- package/lib/builder/tone-presets.js +75 -0
- package/lib/builder/tool-executors.js +1527 -0
- package/lib/builder/tools.js +338 -0
- package/lib/builder/validators.js +239 -0
- package/lib/composer/composer.js +225 -0
- package/lib/composer/index.js +40 -0
- package/lib/composer/protocols/00_base.txt +19 -0
- package/lib/composer/protocols/01_knowledge.txt +9 -0
- package/lib/composer/protocols/02_form-gathering.txt +32 -0
- package/lib/composer/protocols/03_appointments.txt +16 -0
- package/lib/composer/protocols/04_triage.txt +15 -0
- package/lib/composer/protocols/05_optical-read.txt +22 -0
- package/lib/composer/response-builder.js +98 -0
- package/lib/config-builder.js +650 -0
- package/lib/db/ids.js +10 -0
- package/lib/db/index.js +179 -0
- package/lib/db/repositories/apiKeys.js +72 -0
- package/lib/db/repositories/auditLogs.js +12 -0
- package/lib/db/repositories/botSpaces.js +12 -0
- package/lib/db/repositories/builderSessions.js +312 -0
- package/lib/db/repositories/deploymentEvents.js +12 -0
- package/lib/db/repositories/deployments.js +385 -0
- package/lib/db/repositories/documents.js +68 -0
- package/lib/db/repositories/mcpJobs.js +84 -0
- package/lib/deployers/bot-fleet.js +110 -0
- package/lib/deployers/bot-proxy.js +72 -0
- package/lib/deployers/build.js +89 -0
- package/lib/deployers/cloud-deploy.js +310 -0
- package/lib/deployers/docker.js +439 -0
- package/lib/deployers/fly.js +432 -0
- package/lib/deployers/index.js +38 -0
- package/lib/deployment-auth.js +36 -0
- package/lib/document-parser.js +171 -0
- package/lib/embedder/chunker.js +93 -0
- package/lib/embedder/local.js +101 -0
- package/lib/embedder/preview-rag.js +93 -0
- package/lib/envelope-schema.js +54 -0
- package/lib/fleet/scoped-sql.js +342 -0
- package/lib/form-schema-config/base.js +135 -0
- package/lib/form-schema-config/index.js +286 -0
- package/lib/form-schema-config/locales/af-ZA.js +153 -0
- package/lib/form-schema-config/locales/ar-AE.js +142 -0
- package/lib/form-schema-config/locales/ar-SA.js +164 -0
- package/lib/form-schema-config/locales/de-DE.js +152 -0
- package/lib/form-schema-config/locales/en-AU.js +161 -0
- package/lib/form-schema-config/locales/en-CA.js +115 -0
- package/lib/form-schema-config/locales/en-GB.js +132 -0
- package/lib/form-schema-config/locales/en-IN.js +219 -0
- package/lib/form-schema-config/locales/en-MY.js +171 -0
- package/lib/form-schema-config/locales/en-NG.js +198 -0
- package/lib/form-schema-config/locales/en-PH.js +186 -0
- package/lib/form-schema-config/locales/en-SG.js +153 -0
- package/lib/form-schema-config/locales/en-US.js +138 -0
- package/lib/form-schema-config/locales/es-ES.js +171 -0
- package/lib/form-schema-config/locales/es-MX.js +193 -0
- package/lib/form-schema-config/locales/fr-CA.js +138 -0
- package/lib/form-schema-config/locales/fr-FR.js +155 -0
- package/lib/form-schema-config/locales/hi-IN.js +219 -0
- package/lib/form-schema-config/locales/it-IT.js +157 -0
- package/lib/form-schema-config/locales/ja-JP.js +169 -0
- package/lib/form-schema-config/locales/ko-KR.js +140 -0
- package/lib/form-schema-config/locales/nl-NL.js +149 -0
- package/lib/form-schema-config/locales/pt-BR.js +168 -0
- package/lib/form-schema-config/locales/zh-CN.js +172 -0
- package/lib/form-schema-config/locales/zh-HK.js +142 -0
- package/lib/form-structure-schema.js +191 -0
- package/lib/llm-providers.js +828 -0
- package/lib/markdown.js +197 -0
- package/lib/mcp/catalysts/appointment-to-calendar.md +84 -0
- package/lib/mcp/catalysts/conversations-to-channel-digest.md +104 -0
- package/lib/mcp/catalysts/document-extract-to-store.md +92 -0
- package/lib/mcp/catalysts/knowledge-gap-miner.md +96 -0
- package/lib/mcp/catalysts/loader.js +144 -0
- package/lib/mcp/catalysts/qualify-lead-to-crm.md +83 -0
- package/lib/mcp/catalysts/scan-conversations-for-signal.md +92 -0
- package/lib/mcp/catalysts/submission-to-ticket.md +83 -0
- package/lib/mcp/catalysts/submissions-to-warehouse.md +103 -0
- package/lib/mcp/catalysts/weekly-submissions-digest.md +82 -0
- package/lib/mcp/jobs.js +64 -0
- package/lib/mcp/server.js +184 -0
- package/lib/mcp/session-binding.js +130 -0
- package/lib/mcp/tools/build.js +123 -0
- package/lib/mcp/tools/catalysts.js +477 -0
- package/lib/mcp/tools/context.js +325 -0
- package/lib/mcp/tools/fleet.js +391 -0
- package/lib/mcp/tools/jobs-tools.js +240 -0
- package/lib/mcp/tools/operate.js +314 -0
- package/lib/preview/build-preview-config.js +136 -0
- package/lib/rate-limiter.js +11 -0
- package/lib/resolve-api-key.js +142 -0
- package/lib/storage/index.js +40 -0
- package/messages/de.json +2136 -0
- package/messages/en.json +2136 -0
- package/messages/es.json +2136 -0
- package/messages/fr.json +2136 -0
- package/messages/it.json +2136 -0
- package/messages/ja.json +2136 -0
- package/messages/ko.json +2136 -0
- package/messages/nl.json +2136 -0
- package/messages/pl.json +2136 -0
- package/messages/pt.json +2136 -0
- package/messages/ru.json +2136 -0
- package/messages/uk.json +2136 -0
- package/messages/zh.json +2136 -0
- package/package.json +68 -5
- package/scripts/mcp-config.mjs +162 -0
- package/scripts/mcp-stdio-loader.mjs +42 -0
- package/scripts/mcp-stdio.mjs +108 -0
- package/scripts/mojulo-paths.mjs +48 -0
package/lib/mcp/jobs.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process job runner for long-running MCP tool calls.
|
|
3
|
+
*
|
|
4
|
+
* MCP clients don't all implement progress notifications, and many tool
|
|
5
|
+
* libraries enforce a "tools must return in N seconds" pattern. We wrap
|
|
6
|
+
* anything >2s as a job: returns { jobId } immediately, the model polls
|
|
7
|
+
* with `poll_job`.
|
|
8
|
+
*
|
|
9
|
+
* Storage: `mcp_jobs` table (see lib/db/index.js). On control-plane restart,
|
|
10
|
+
* pending / running jobs are reaped to `error` state — that's all the
|
|
11
|
+
* recovery the single-user posture needs.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { McpJobRepository, JOB_STATUS } from '@/lib/db/repositories/mcpJobs';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Start a job. Returns immediately with { jobId, status: 'pending' }.
|
|
18
|
+
* The provided `task(jobId)` runs in the background; its return value becomes
|
|
19
|
+
* the job result, its thrown error becomes the job error.
|
|
20
|
+
*
|
|
21
|
+
* `task` can optionally call McpJobRepository.setProgress(jobId, pct) to
|
|
22
|
+
* report progress for clients that poll.
|
|
23
|
+
*/
|
|
24
|
+
export async function startJob({
|
|
25
|
+
tool,
|
|
26
|
+
mcpSessionId = null,
|
|
27
|
+
builderSessionId = null,
|
|
28
|
+
task,
|
|
29
|
+
}) {
|
|
30
|
+
if (typeof task !== 'function') {
|
|
31
|
+
throw new Error('startJob requires a task function');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const job = await McpJobRepository.create({ tool, mcpSessionId, builderSessionId });
|
|
35
|
+
|
|
36
|
+
// Don't await — fire and forget; the caller returns the jobId to the client.
|
|
37
|
+
runJobInBackground(job.id, task);
|
|
38
|
+
|
|
39
|
+
return { jobId: job.id, status: job.status };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runJobInBackground(jobId, task) {
|
|
43
|
+
// setImmediate so the create() write commits and the HTTP response goes out
|
|
44
|
+
// before we start the heavy work. queueMicrotask wouldn't be enough — the
|
|
45
|
+
// task can be CPU-bound on the embedder.
|
|
46
|
+
setImmediate(async () => {
|
|
47
|
+
try {
|
|
48
|
+
await McpJobRepository.setRunning(jobId);
|
|
49
|
+
const result = await task(jobId);
|
|
50
|
+
await McpJobRepository.setDone(jobId, result);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error(`[mcp:job ${jobId}] failed:`, err);
|
|
53
|
+
await McpJobRepository.setError(jobId, err).catch((dbErr) => {
|
|
54
|
+
console.error(`[mcp:job ${jobId}] failed to persist error:`, dbErr);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function getJob(jobId) {
|
|
61
|
+
return McpJobRepository.findById(jobId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { JOB_STATUS };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server core — protocol dispatch and tool registry.
|
|
3
|
+
*
|
|
4
|
+
* The Next.js route ([api/mcp/route.js]) handles HTTP + bearer auth and
|
|
5
|
+
* forwards parsed JSON-RPC messages here. This module owns the MCP
|
|
6
|
+
* protocol semantics: initialize / tools/list / tools/call.
|
|
7
|
+
*
|
|
8
|
+
* Tools are registered in rings (see [tools/build.js], [tools/operate.js]).
|
|
9
|
+
* Each registered tool has:
|
|
10
|
+
* - name, description, inputSchema (JSON Schema)
|
|
11
|
+
* - handler(input, context) → result | Promise<result>
|
|
12
|
+
*
|
|
13
|
+
* Execution context carries:
|
|
14
|
+
* - mcpSessionId — used by session-binding.js to attach a BuilderSession
|
|
15
|
+
* - userId — always 'local' (single-user posture, see auth/service.js)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const PROTOCOL_VERSION = '2024-11-05';
|
|
19
|
+
const SERVER_NAME = 'mojulo-control-plane';
|
|
20
|
+
const SERVER_VERSION = '0.1.0';
|
|
21
|
+
|
|
22
|
+
// Surfaced to the connecting model on `initialize`. Most MCP clients hand this
|
|
23
|
+
// to the agent as a system-prompt-style preamble — it has to fit and stick
|
|
24
|
+
// even on clients that truncate aggressively. We keep it deliberately short
|
|
25
|
+
// and noun-free: one framing sentence + one explicit pointer to load the full
|
|
26
|
+
// briefing on demand. The heavy lifting (glossary, capability model,
|
|
27
|
+
// lifecycle, tool index) lives in the `forward_context` tool's response so
|
|
28
|
+
// the agent only pays the context cost when the user actually needs it.
|
|
29
|
+
const SERVER_INSTRUCTIONS = `Mojulo is a control plane for **chatbot-based solutions** — chatbots that talk to your users, capture what they say, and turn those conversations into real outcomes in the tools the user already runs (CRM, calendar, ticketing, drive, warehouse).
|
|
30
|
+
|
|
31
|
+
**When the user asks what mojulo is, how it works, or which tools to pick — call \`forward_context\` first.** It returns the concept glossary, the bot capability model, the deploy/connect lifecycle, and a one-line description of every tool, so you can orient before acting.`;
|
|
32
|
+
|
|
33
|
+
const registeredTools = new Map();
|
|
34
|
+
|
|
35
|
+
export function registerTool(tool) {
|
|
36
|
+
if (!tool || !tool.name || typeof tool.handler !== 'function') {
|
|
37
|
+
throw new Error('registerTool requires { name, handler }');
|
|
38
|
+
}
|
|
39
|
+
registeredTools.set(tool.name, tool);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function listTools() {
|
|
43
|
+
return Array.from(registeredTools.values()).map((t) => ({
|
|
44
|
+
name: t.name,
|
|
45
|
+
description: t.description || '',
|
|
46
|
+
inputSchema: t.inputSchema || { type: 'object', properties: {} },
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function jsonRpcResult(id, result) {
|
|
51
|
+
return { jsonrpc: '2.0', id, result };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function jsonRpcError(id, code, message, data) {
|
|
55
|
+
const err = { code, message };
|
|
56
|
+
if (data !== undefined) err.data = data;
|
|
57
|
+
return { jsonrpc: '2.0', id, error: err };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ErrorCodes = {
|
|
61
|
+
PARSE_ERROR: -32700,
|
|
62
|
+
INVALID_REQUEST: -32600,
|
|
63
|
+
METHOD_NOT_FOUND: -32601,
|
|
64
|
+
INVALID_PARAMS: -32602,
|
|
65
|
+
INTERNAL_ERROR: -32603,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export async function dispatchMcpRequest(message, context) {
|
|
69
|
+
if (!message || message.jsonrpc !== '2.0') {
|
|
70
|
+
return jsonRpcError(message?.id ?? null, ErrorCodes.INVALID_REQUEST, 'Invalid JSON-RPC request');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Notifications (no id) — we accept and return nothing.
|
|
74
|
+
const isNotification = message.id === undefined || message.id === null;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
switch (message.method) {
|
|
78
|
+
case 'initialize':
|
|
79
|
+
return isNotification
|
|
80
|
+
? null
|
|
81
|
+
: jsonRpcResult(message.id, {
|
|
82
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
83
|
+
capabilities: {
|
|
84
|
+
tools: { listChanged: false },
|
|
85
|
+
},
|
|
86
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
87
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
case 'notifications/initialized':
|
|
91
|
+
case 'initialized':
|
|
92
|
+
return null;
|
|
93
|
+
|
|
94
|
+
case 'ping':
|
|
95
|
+
return isNotification ? null : jsonRpcResult(message.id, {});
|
|
96
|
+
|
|
97
|
+
case 'tools/list':
|
|
98
|
+
return jsonRpcResult(message.id, { tools: listTools() });
|
|
99
|
+
|
|
100
|
+
case 'tools/call':
|
|
101
|
+
return await handleToolCall(message, context);
|
|
102
|
+
|
|
103
|
+
default:
|
|
104
|
+
if (isNotification) return null;
|
|
105
|
+
return jsonRpcError(
|
|
106
|
+
message.id,
|
|
107
|
+
ErrorCodes.METHOD_NOT_FOUND,
|
|
108
|
+
`Method not found: ${message.method}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error('[mcp] dispatch error:', err);
|
|
113
|
+
if (isNotification) return null;
|
|
114
|
+
return jsonRpcError(
|
|
115
|
+
message.id,
|
|
116
|
+
ErrorCodes.INTERNAL_ERROR,
|
|
117
|
+
err.message || 'Internal error'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function handleToolCall(message, context) {
|
|
123
|
+
const params = message.params || {};
|
|
124
|
+
const toolName = params.name;
|
|
125
|
+
const toolInput = params.arguments || {};
|
|
126
|
+
|
|
127
|
+
const tool = registeredTools.get(toolName);
|
|
128
|
+
if (!tool) {
|
|
129
|
+
return jsonRpcError(
|
|
130
|
+
message.id,
|
|
131
|
+
ErrorCodes.METHOD_NOT_FOUND,
|
|
132
|
+
`Unknown tool: ${toolName}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const result = await tool.handler(toolInput, context);
|
|
138
|
+
return jsonRpcResult(message.id, toMcpToolResult(result));
|
|
139
|
+
} catch (err) {
|
|
140
|
+
// Per MCP spec, tool execution failures are returned as a tool_result
|
|
141
|
+
// with isError: true rather than a JSON-RPC error — so the client model
|
|
142
|
+
// can see the failure and react.
|
|
143
|
+
return jsonRpcResult(message.id, {
|
|
144
|
+
content: [{ type: 'text', text: err.message || 'Tool execution failed' }],
|
|
145
|
+
isError: true,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function toMcpToolResult(result) {
|
|
151
|
+
if (result && typeof result === 'object' && Array.isArray(result.content)) {
|
|
152
|
+
// Tool already returned MCP-shaped content; trust it.
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
const text =
|
|
156
|
+
typeof result === 'string' ? result : JSON.stringify(result ?? {}, null, 2);
|
|
157
|
+
return { content: [{ type: 'text', text }] };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Tool registrations run on first request rather than at module load. We use
|
|
161
|
+
// dynamic import to avoid a circular dependency: tool modules import
|
|
162
|
+
// `registerTool` from this file.
|
|
163
|
+
let _registered = false;
|
|
164
|
+
export async function ensureToolsRegistered() {
|
|
165
|
+
if (_registered) return;
|
|
166
|
+
_registered = true;
|
|
167
|
+
const { registerContextTools } = await import('@/lib/mcp/tools/context');
|
|
168
|
+
const { registerBuildTools } = await import('@/lib/mcp/tools/build');
|
|
169
|
+
const { registerJobsTools } = await import('@/lib/mcp/tools/jobs-tools');
|
|
170
|
+
const { registerOperateTools } = await import('@/lib/mcp/tools/operate');
|
|
171
|
+
const { registerFleetTools } = await import('@/lib/mcp/tools/fleet');
|
|
172
|
+
const { registerCatalystTools } = await import('@/lib/mcp/tools/catalysts');
|
|
173
|
+
// Order matters only for tools/list output (insertion order). Putting
|
|
174
|
+
// forward_context first means clients that surface the tool list to the
|
|
175
|
+
// model see the orientation tool at the top. Fleet tools sit between
|
|
176
|
+
// per-bot operate and catalysts so the natural reading order is
|
|
177
|
+
// per-bot → fleet → outcome.
|
|
178
|
+
registerContextTools();
|
|
179
|
+
registerBuildTools();
|
|
180
|
+
registerJobsTools();
|
|
181
|
+
registerOperateTools();
|
|
182
|
+
registerFleetTools();
|
|
183
|
+
registerCatalystTools();
|
|
184
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy BuilderSession binding for MCP connections.
|
|
3
|
+
*
|
|
4
|
+
* The web chat-builder threads a `session_id` through every tool call;
|
|
5
|
+
* MCP doesn't have an analogous primitive that's load-bearing for our flow.
|
|
6
|
+
* Instead, we bind one BuilderSession per `mcp-session-id` header value,
|
|
7
|
+
* creating it on first build-tool invocation and reusing across calls.
|
|
8
|
+
*
|
|
9
|
+
* On reconnect / restart, in-memory map is lost; the user's Claude effectively
|
|
10
|
+
* starts a new bot. Mirrors the web flow's "tab closed = session orphaned"
|
|
11
|
+
* behavior. See plan section 9 for the upgrade path (persist + resume).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { BuilderSessionRepository } from '@/lib/db/repositories/builderSessions';
|
|
15
|
+
import { DocumentRepository } from '@/lib/db/repositories/documents';
|
|
16
|
+
import { DeploymentRepository } from '@/lib/db/repositories/deployments';
|
|
17
|
+
import { ApiKeyRepository } from '@/lib/db/repositories/apiKeys';
|
|
18
|
+
import { getDefaultModelForTask } from '@/lib/llm-providers';
|
|
19
|
+
|
|
20
|
+
const ANTHROPIC_DEFAULT_MODEL = 'claude-sonnet-4-6';
|
|
21
|
+
|
|
22
|
+
// The api_keys table mixes LLM provider keys with cloud-deploy provider keys
|
|
23
|
+
// (Fly.io's API token lives under provider='fly'; see lib/deployers/cloud-deploy.js).
|
|
24
|
+
// When picking a *default LLM* we must filter to providers the chat / builder
|
|
25
|
+
// LLM call sites actually handle — otherwise generate_form_schema and friends
|
|
26
|
+
// throw "Unsupported provider: fly".
|
|
27
|
+
const LLM_PROVIDERS = new Set(['anthropic', 'openai', 'ollama']);
|
|
28
|
+
|
|
29
|
+
// mcpSessionId → BuilderSession.id
|
|
30
|
+
const bindings = new Map();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Mirror of the web flow's buildPreloadedContext (api/builder/stream/route.js).
|
|
34
|
+
* Loaded onto every fresh BuilderSession so tool executors can read the
|
|
35
|
+
* default provider, API key, and workspace docs.
|
|
36
|
+
*/
|
|
37
|
+
async function buildPreloadedContext(userId) {
|
|
38
|
+
const [documents, apiKeys] = await Promise.all([
|
|
39
|
+
DocumentRepository.findByBotSpaceId(null),
|
|
40
|
+
ApiKeyRepository.findByUserId(userId),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const existingBots = await DeploymentRepository.list();
|
|
44
|
+
|
|
45
|
+
const llmKeys = apiKeys.filter((k) => LLM_PROVIDERS.has(k.provider));
|
|
46
|
+
|
|
47
|
+
let defaultProvider;
|
|
48
|
+
let defaultModel;
|
|
49
|
+
let defaultApiKeyId;
|
|
50
|
+
const defaultKey = llmKeys.find((k) => k.isDefault) || llmKeys[0];
|
|
51
|
+
if (defaultKey) {
|
|
52
|
+
defaultProvider = defaultKey.provider;
|
|
53
|
+
defaultModel = getDefaultModelForTask(defaultKey.provider, 'reasoning');
|
|
54
|
+
defaultApiKeyId = defaultKey.id;
|
|
55
|
+
} else {
|
|
56
|
+
defaultProvider = 'anthropic';
|
|
57
|
+
defaultModel = ANTHROPIC_DEFAULT_MODEL;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
organizationName: 'Local',
|
|
62
|
+
workspaceName: 'Mojulo-Lite',
|
|
63
|
+
workspaceDocuments: documents.map((d) => ({
|
|
64
|
+
id: d.id,
|
|
65
|
+
name: d.originalName,
|
|
66
|
+
mimeType: d.mimeType,
|
|
67
|
+
})),
|
|
68
|
+
existingBots: existingBots
|
|
69
|
+
.filter((d) => d.status === 'ready' && d.url)
|
|
70
|
+
.map((d) => ({
|
|
71
|
+
botName: d.botName,
|
|
72
|
+
id: d.id,
|
|
73
|
+
url: d.url,
|
|
74
|
+
botSummary: d.config?.botSummary || null,
|
|
75
|
+
})),
|
|
76
|
+
defaultProvider,
|
|
77
|
+
defaultModel,
|
|
78
|
+
defaultApiKeyId,
|
|
79
|
+
apiKeys: apiKeys.map((k) => ({
|
|
80
|
+
id: k.id,
|
|
81
|
+
name: k.name,
|
|
82
|
+
provider: k.provider,
|
|
83
|
+
})),
|
|
84
|
+
disableModuloAnimation: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get or create the BuilderSession for this MCP connection. Always refreshes
|
|
90
|
+
* the session row from SQLite so tool handlers see writes from prior tool
|
|
91
|
+
* calls in the same connection.
|
|
92
|
+
*/
|
|
93
|
+
export async function getOrCreateBuilderSession(mcpSessionId, userId) {
|
|
94
|
+
const existingId = bindings.get(mcpSessionId);
|
|
95
|
+
if (existingId) {
|
|
96
|
+
const session = await BuilderSessionRepository.findById(existingId);
|
|
97
|
+
if (session) return session;
|
|
98
|
+
bindings.delete(mcpSessionId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const apiKeys = await ApiKeyRepository.findByUserId(userId);
|
|
102
|
+
const hasLlmKey = apiKeys.some((k) => LLM_PROVIDERS.has(k.provider));
|
|
103
|
+
if (!hasLlmKey) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
'No LLM provider key configured on the control plane. Add an Anthropic / OpenAI / Ollama key at /settings before using the MCP build tools. (Cloud-deploy tokens like Fly do not count.)'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const preloadedContext = await buildPreloadedContext(userId);
|
|
110
|
+
const session = await BuilderSessionRepository.createWithContext({
|
|
111
|
+
userId,
|
|
112
|
+
botSpaceId: null,
|
|
113
|
+
preloadedContext,
|
|
114
|
+
});
|
|
115
|
+
bindings.set(mcpSessionId, session.id);
|
|
116
|
+
return session;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Force a new BuilderSession on the next build-tool call. Used by the
|
|
121
|
+
* `start_new_bot` MCP tool so the user's Claude can build a second bot in
|
|
122
|
+
* the same MCP connection without restarting the client.
|
|
123
|
+
*/
|
|
124
|
+
export function resetBuilderSession(mcpSessionId) {
|
|
125
|
+
bindings.delete(mcpSessionId);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getBoundSessionId(mcpSessionId) {
|
|
129
|
+
return bindings.get(mcpSessionId) || null;
|
|
130
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Ring 1 — build tools.
|
|
3
|
+
*
|
|
4
|
+
* Each tool here is a thin wrapper around a handler in
|
|
5
|
+
* [@/lib/builder/tool-executors](control/lib/builder/tool-executors.js).
|
|
6
|
+
* The wrapper:
|
|
7
|
+
* 1. Lazily binds a BuilderSession to the MCP connection (first call).
|
|
8
|
+
* 2. Refreshes the session row so writes from prior tool calls are visible.
|
|
9
|
+
* 3. Delegates to executeBuilderTool, surfaces success / error to MCP.
|
|
10
|
+
*
|
|
11
|
+
* Tool schemas are derived from BUILDER_TOOLS — single source of truth lives
|
|
12
|
+
* in [@/lib/builder/tools](control/lib/builder/tools.js). The MCP `inputSchema`
|
|
13
|
+
* is the same JSON Schema the web chat builder gives Claude, so the user's
|
|
14
|
+
* Claude sees an identical tool surface.
|
|
15
|
+
*
|
|
16
|
+
* Phase 1 registers synchronous tools only. `process_documents` and
|
|
17
|
+
* `save_modular_bot` are deferred to Phase 2 (job-based) because they can run
|
|
18
|
+
* >2s and need a poll interface.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { BUILDER_TOOLS } from '@/lib/builder/tools';
|
|
22
|
+
import { executeBuilderTool } from '@/lib/builder/tool-executors';
|
|
23
|
+
import { BuilderSessionRepository } from '@/lib/db/repositories/builderSessions';
|
|
24
|
+
import {
|
|
25
|
+
getOrCreateBuilderSession,
|
|
26
|
+
resetBuilderSession,
|
|
27
|
+
} from '@/lib/mcp/session-binding';
|
|
28
|
+
import { registerTool } from '@/lib/mcp/server';
|
|
29
|
+
|
|
30
|
+
const SYNC_TOOL_NAMES = new Set([
|
|
31
|
+
'infer_intent',
|
|
32
|
+
'recommend_protocols',
|
|
33
|
+
'generate_form_schema',
|
|
34
|
+
'generate_appointment_config',
|
|
35
|
+
'generate_triage_config',
|
|
36
|
+
'generate_optical_read_config',
|
|
37
|
+
'compose_identity',
|
|
38
|
+
'set_suggested_prompts',
|
|
39
|
+
'generate_bot_summary',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
function findBuilderToolSchema(name) {
|
|
43
|
+
const tool = BUILDER_TOOLS.find((t) => t.name === name);
|
|
44
|
+
if (!tool) throw new Error(`BUILDER_TOOLS is missing ${name}`);
|
|
45
|
+
return tool;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build a context the existing tool executors expect: { session, userId }.
|
|
50
|
+
* The session is re-fetched on every call to pick up writes from prior tool
|
|
51
|
+
* invocations in the same MCP connection (each call mutates SQLite, but the
|
|
52
|
+
* in-memory object would be stale otherwise).
|
|
53
|
+
*/
|
|
54
|
+
async function buildExecutorContext(mcpContext) {
|
|
55
|
+
const session = await getOrCreateBuilderSession(
|
|
56
|
+
mcpContext.mcpSessionId,
|
|
57
|
+
mcpContext.userId
|
|
58
|
+
);
|
|
59
|
+
return { session, userId: mcpContext.userId };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function makeHandler(toolName) {
|
|
63
|
+
return async function handle(input, mcpContext) {
|
|
64
|
+
const ctx = await buildExecutorContext(mcpContext);
|
|
65
|
+
const result = await executeBuilderTool(toolName, input, ctx);
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
throw new Error(result.error || `${toolName} failed`);
|
|
68
|
+
}
|
|
69
|
+
return result.result;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function registerBuildTools() {
|
|
74
|
+
for (const name of SYNC_TOOL_NAMES) {
|
|
75
|
+
const schema = findBuilderToolSchema(name);
|
|
76
|
+
registerTool({
|
|
77
|
+
name: schema.name,
|
|
78
|
+
description: schema.description,
|
|
79
|
+
inputSchema: schema.input_schema,
|
|
80
|
+
handler: makeHandler(name),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Ergonomic extra not in BUILDER_TOOLS — lets the user's Claude start over
|
|
85
|
+
// without dropping the MCP connection. Mirrors closing/reopening the web
|
|
86
|
+
// chat-builder tab.
|
|
87
|
+
registerTool({
|
|
88
|
+
name: 'start_new_bot',
|
|
89
|
+
description:
|
|
90
|
+
'Reset the builder session for this MCP connection so the next build tool call starts a fresh bot from scratch. Use when the user wants to build a second bot in the same session, or to discard in-progress configuration.',
|
|
91
|
+
inputSchema: { type: 'object', properties: {} },
|
|
92
|
+
handler: async (_input, mcpContext) => {
|
|
93
|
+
resetBuilderSession(mcpContext.mcpSessionId);
|
|
94
|
+
return { message: 'Builder session reset. The next build tool call will start a new bot.' };
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Lets the user's Claude inspect the in-progress configuration without
|
|
99
|
+
// peeking at the SQLite row directly. Read-only, no session mutation.
|
|
100
|
+
registerTool({
|
|
101
|
+
name: 'get_builder_session',
|
|
102
|
+
description:
|
|
103
|
+
'Return the current builder session state for this MCP connection — inferred intent, recommended protocols, identity, generated configs. Useful for the model to see what it has built so far before composing the final save.',
|
|
104
|
+
inputSchema: { type: 'object', properties: {} },
|
|
105
|
+
handler: async (_input, mcpContext) => {
|
|
106
|
+
const session = await getOrCreateBuilderSession(
|
|
107
|
+
mcpContext.mcpSessionId,
|
|
108
|
+
mcpContext.userId
|
|
109
|
+
);
|
|
110
|
+
const fresh = await BuilderSessionRepository.findById(session.id);
|
|
111
|
+
return {
|
|
112
|
+
sessionId: fresh.id,
|
|
113
|
+
status: fresh.status,
|
|
114
|
+
inferredIntent: fresh.inferredIntent,
|
|
115
|
+
intentConfidence: fresh.intentConfidence,
|
|
116
|
+
recommendedProtocols: fresh.recommendedProtocols,
|
|
117
|
+
enabledProtocols: fresh.enabledProtocols,
|
|
118
|
+
generatedConfigs: fresh.generatedConfigs,
|
|
119
|
+
deploymentId: fresh.deploymentId,
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|