lucent-ui 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{LucentProvider-F0EN_7TD.js → LucentProvider-Bm39MMvv.js} +550 -532
- package/dist/{LucentProvider-LqNc0AxD.cjs → LucentProvider-CzEDW5SL.cjs} +6 -6
- package/dist/devtools.cjs +1 -1
- package/dist/devtools.js +1 -1
- package/dist/index.cjs +87 -41
- package/dist/index.d.ts +92 -5
- package/dist/index.js +1528 -1293
- package/dist-server/server/http.js +126 -0
- package/dist-server/server/index.js +2 -293
- package/dist-server/server/logger.js +66 -0
- package/dist-server/server/tools.js +298 -0
- package/dist-server/src/components/atoms/Row/Row.manifest.js +10 -2
- package/dist-server/src/components/atoms/Stack/Stack.manifest.js +6 -2
- package/dist-server/src/components/atoms/Table/Table.manifest.js +4 -1
- package/dist-server/src/components/molecules/Breadcrumb/Breadcrumb.manifest.js +27 -0
- package/dist-server/src/components/molecules/DataTable/DataTable.manifest.js +3 -1
- package/dist-server/src/components/molecules/FileUpload/FileUpload.manifest.js +2 -0
- package/dist-server/src/components/molecules/PageHeader/PageHeader.manifest.js +144 -0
- package/dist-server/src/manifest/examples/button.manifest.js +39 -1
- package/dist-server/src/manifest/patterns/action-bar.pattern.js +34 -6
- package/package.json +2 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
5
|
+
import { registerTools, DESIGN_RULES_SUMMARY } from './tools.js';
|
|
6
|
+
const PORT = Number(process.env['PORT'] ?? 3000);
|
|
7
|
+
const HOST = process.env['HOST'] ?? '127.0.0.1';
|
|
8
|
+
const API_KEY = process.env['LUCENT_API_KEY'];
|
|
9
|
+
const MCP_PATH = process.env['LUCENT_MCP_PATH'] ?? '/mcp';
|
|
10
|
+
function log(msg) {
|
|
11
|
+
process.stderr.write(`[lucent-mcp-http] ${msg}\n`);
|
|
12
|
+
}
|
|
13
|
+
// Build a fresh McpServer + tool registrations per request (stateless mode).
|
|
14
|
+
function createServerInstance() {
|
|
15
|
+
const server = new McpServer({ name: 'lucent-mcp', version: '0.1.0' }, { instructions: DESIGN_RULES_SUMMARY });
|
|
16
|
+
registerTools(server);
|
|
17
|
+
return server;
|
|
18
|
+
}
|
|
19
|
+
function readJsonBody(req) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
23
|
+
req.on('end', () => {
|
|
24
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
25
|
+
if (!raw)
|
|
26
|
+
return resolve(undefined);
|
|
27
|
+
try {
|
|
28
|
+
resolve(JSON.parse(raw));
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
req.on('error', reject);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function writeJson(res, status, body) {
|
|
38
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
39
|
+
res.end(JSON.stringify(body));
|
|
40
|
+
}
|
|
41
|
+
function jsonRpcError(res, status, code, message) {
|
|
42
|
+
writeJson(res, status, {
|
|
43
|
+
jsonrpc: '2.0',
|
|
44
|
+
error: { code, message },
|
|
45
|
+
id: null,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function checkAuth(req) {
|
|
49
|
+
if (!API_KEY)
|
|
50
|
+
return true; // Auth disabled when env var is not set
|
|
51
|
+
const header = req.headers['authorization'];
|
|
52
|
+
if (typeof header !== 'string')
|
|
53
|
+
return false;
|
|
54
|
+
const match = header.match(/^Bearer\s+(.+)$/i);
|
|
55
|
+
return match !== null && match[1] === API_KEY;
|
|
56
|
+
}
|
|
57
|
+
function setCorsHeaders(res) {
|
|
58
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
59
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, OPTIONS');
|
|
60
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id');
|
|
61
|
+
}
|
|
62
|
+
const httpServer = createServer(async (req, res) => {
|
|
63
|
+
setCorsHeaders(res);
|
|
64
|
+
// CORS preflight
|
|
65
|
+
if (req.method === 'OPTIONS') {
|
|
66
|
+
res.writeHead(204, { 'Access-Control-Max-Age': '86400' });
|
|
67
|
+
res.end();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
71
|
+
// Health check — unauthenticated, useful for uptime probes
|
|
72
|
+
if (url.pathname === '/health' && req.method === 'GET') {
|
|
73
|
+
writeJson(res, 200, { status: 'ok' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (url.pathname !== MCP_PATH) {
|
|
77
|
+
writeJson(res, 404, { error: 'Not found' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!checkAuth(req)) {
|
|
81
|
+
res.setHeader('WWW-Authenticate', 'Bearer');
|
|
82
|
+
writeJson(res, 401, { error: 'Unauthorized' });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Only POST is supported in stateless mode (no server-initiated streams).
|
|
86
|
+
if (req.method !== 'POST') {
|
|
87
|
+
jsonRpcError(res, 405, -32000, 'Method not allowed.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const body = await readJsonBody(req);
|
|
92
|
+
const server = createServerInstance();
|
|
93
|
+
const transport = new StreamableHTTPServerTransport({}); // stateless
|
|
94
|
+
// SDK types widen onclose/onerror with `| undefined`, which trips
|
|
95
|
+
// exactOptionalPropertyTypes against the stricter Transport interface.
|
|
96
|
+
await server.connect(transport);
|
|
97
|
+
res.on('close', () => {
|
|
98
|
+
void transport.close();
|
|
99
|
+
void server.close();
|
|
100
|
+
});
|
|
101
|
+
await transport.handleRequest(req, res, body);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
log(`error handling request: ${err.message}`);
|
|
105
|
+
if (!res.headersSent) {
|
|
106
|
+
jsonRpcError(res, 500, -32603, 'Internal server error');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
httpServer.listen(PORT, HOST, () => {
|
|
111
|
+
log(`listening on http://${HOST}:${PORT}${MCP_PATH}`);
|
|
112
|
+
if (!API_KEY) {
|
|
113
|
+
log('WARNING: LUCENT_API_KEY is not set — the server is unauthenticated.');
|
|
114
|
+
}
|
|
115
|
+
if (HOST === '0.0.0.0' || HOST === '::') {
|
|
116
|
+
log(`WARNING: bound to ${HOST}. Set LUCENT_API_KEY before exposing publicly.`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
function shutdown() {
|
|
120
|
+
log('shutting down...');
|
|
121
|
+
httpServer.close(() => process.exit(0));
|
|
122
|
+
// Force-exit if graceful close hangs
|
|
123
|
+
setTimeout(() => process.exit(1), 5000).unref();
|
|
124
|
+
}
|
|
125
|
+
process.on('SIGINT', shutdown);
|
|
126
|
+
process.on('SIGTERM', shutdown);
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import {
|
|
5
|
-
import { ALL_MANIFESTS } from './registry.js';
|
|
6
|
-
import { ALL_PATTERNS } from './pattern-registry.js';
|
|
7
|
-
import { PALETTES, SHAPES, DENSITIES, SHADOWS, COMBINED, generatePresetConfig } from './presets.js';
|
|
8
|
-
import { DESIGN_RULES, DESIGN_RULES_SUMMARY } from './design-rules.js';
|
|
4
|
+
import { registerTools, DESIGN_RULES_SUMMARY } from './tools.js';
|
|
9
5
|
// ─── Auth stub ───────────────────────────────────────────────────────────────
|
|
10
6
|
// LUCENT_API_KEY is reserved for the future paid tier.
|
|
11
7
|
// When set, the server acknowledges it but does not yet enforce it.
|
|
@@ -13,55 +9,6 @@ const apiKey = process.env['LUCENT_API_KEY'];
|
|
|
13
9
|
if (apiKey) {
|
|
14
10
|
process.stderr.write('[lucent-mcp] Auth mode active (LUCENT_API_KEY is set).\n');
|
|
15
11
|
}
|
|
16
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
17
|
-
function findManifest(nameOrId) {
|
|
18
|
-
const q = nameOrId.trim().toLowerCase();
|
|
19
|
-
return ALL_MANIFESTS.find((m) => m.id.toLowerCase() === q || m.name.toLowerCase() === q);
|
|
20
|
-
}
|
|
21
|
-
function scoreManifest(m, query) {
|
|
22
|
-
const q = query.toLowerCase();
|
|
23
|
-
let score = 0;
|
|
24
|
-
if (m.name.toLowerCase().includes(q))
|
|
25
|
-
score += 10;
|
|
26
|
-
if (m.id.toLowerCase().includes(q))
|
|
27
|
-
score += 8;
|
|
28
|
-
if (m.tier.toLowerCase().includes(q))
|
|
29
|
-
score += 5;
|
|
30
|
-
if (m.description.toLowerCase().includes(q))
|
|
31
|
-
score += 4;
|
|
32
|
-
if (m.designIntent.toLowerCase().includes(q))
|
|
33
|
-
score += 3;
|
|
34
|
-
for (const p of m.props) {
|
|
35
|
-
if (p.name.toLowerCase().includes(q))
|
|
36
|
-
score += 2;
|
|
37
|
-
if (p.description.toLowerCase().includes(q))
|
|
38
|
-
score += 1;
|
|
39
|
-
}
|
|
40
|
-
return score;
|
|
41
|
-
}
|
|
42
|
-
function findPattern(nameOrId) {
|
|
43
|
-
const q = nameOrId.trim().toLowerCase();
|
|
44
|
-
return ALL_PATTERNS.find((r) => r.id.toLowerCase() === q || r.name.toLowerCase() === q);
|
|
45
|
-
}
|
|
46
|
-
function scorePattern(r, query) {
|
|
47
|
-
const q = query.toLowerCase();
|
|
48
|
-
let score = 0;
|
|
49
|
-
if (r.name.toLowerCase().includes(q))
|
|
50
|
-
score += 10;
|
|
51
|
-
if (r.id.toLowerCase().includes(q))
|
|
52
|
-
score += 8;
|
|
53
|
-
if (r.category.toLowerCase().includes(q))
|
|
54
|
-
score += 5;
|
|
55
|
-
if (r.description.toLowerCase().includes(q))
|
|
56
|
-
score += 4;
|
|
57
|
-
if (r.designNotes.toLowerCase().includes(q))
|
|
58
|
-
score += 3;
|
|
59
|
-
for (const c of r.components) {
|
|
60
|
-
if (c.toLowerCase().includes(q))
|
|
61
|
-
score += 2;
|
|
62
|
-
}
|
|
63
|
-
return score;
|
|
64
|
-
}
|
|
65
12
|
// ─── MCP Server ───────────────────────────────────────────────────────────────
|
|
66
13
|
const server = new McpServer({
|
|
67
14
|
name: 'lucent-mcp',
|
|
@@ -69,245 +16,7 @@ const server = new McpServer({
|
|
|
69
16
|
}, {
|
|
70
17
|
instructions: DESIGN_RULES_SUMMARY,
|
|
71
18
|
});
|
|
72
|
-
|
|
73
|
-
server.tool('list_components', 'Lists all available Lucent UI components with their name, tier (atom/molecule), and one-line description.', {}, async () => {
|
|
74
|
-
const components = ALL_MANIFESTS.map((m) => ({
|
|
75
|
-
id: m.id,
|
|
76
|
-
name: m.name,
|
|
77
|
-
tier: m.tier,
|
|
78
|
-
description: m.description,
|
|
79
|
-
}));
|
|
80
|
-
return {
|
|
81
|
-
content: [
|
|
82
|
-
{
|
|
83
|
-
type: 'text',
|
|
84
|
-
text: JSON.stringify({ components }, null, 2),
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
};
|
|
88
|
-
});
|
|
89
|
-
// Tool: get_component_manifest
|
|
90
|
-
server.tool('get_component_manifest', 'Returns the full manifest JSON for a Lucent UI component, including props, usage examples, design intent, and accessibility notes.', { componentName: z.string().describe('Component name or id, e.g. "Button" or "form-field"') }, async ({ componentName }) => {
|
|
91
|
-
const manifest = findManifest(componentName);
|
|
92
|
-
if (!manifest) {
|
|
93
|
-
return {
|
|
94
|
-
content: [
|
|
95
|
-
{
|
|
96
|
-
type: 'text',
|
|
97
|
-
text: JSON.stringify({
|
|
98
|
-
error: `Component "${componentName}" not found.`,
|
|
99
|
-
available: ALL_MANIFESTS.map((m) => m.name),
|
|
100
|
-
}),
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
isError: true,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
content: [
|
|
108
|
-
{
|
|
109
|
-
type: 'text',
|
|
110
|
-
text: JSON.stringify(manifest, null, 2),
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
};
|
|
114
|
-
});
|
|
115
|
-
// Tool: search_components
|
|
116
|
-
server.tool('search_components', 'Searches Lucent UI components and composition patterns by description or concept. Returns matching components and patterns ranked by relevance.', { query: z.string().describe('Natural language or keyword query, e.g. "loading indicator", "form validation", or "profile card"') }, async ({ query }) => {
|
|
117
|
-
const componentResults = ALL_MANIFESTS
|
|
118
|
-
.map((m) => ({ manifest: m, score: scoreManifest(m, query) }))
|
|
119
|
-
.filter(({ score }) => score > 0)
|
|
120
|
-
.sort((a, b) => b.score - a.score)
|
|
121
|
-
.map(({ manifest, score }) => ({
|
|
122
|
-
id: manifest.id,
|
|
123
|
-
name: manifest.name,
|
|
124
|
-
tier: manifest.tier,
|
|
125
|
-
description: manifest.description,
|
|
126
|
-
score,
|
|
127
|
-
}));
|
|
128
|
-
const patternResults = ALL_PATTERNS
|
|
129
|
-
.map((r) => ({ pattern: r, score: scorePattern(r, query) }))
|
|
130
|
-
.filter(({ score }) => score > 0)
|
|
131
|
-
.sort((a, b) => b.score - a.score)
|
|
132
|
-
.map(({ pattern, score }) => ({
|
|
133
|
-
id: pattern.id,
|
|
134
|
-
name: pattern.name,
|
|
135
|
-
category: pattern.category,
|
|
136
|
-
description: pattern.description,
|
|
137
|
-
score,
|
|
138
|
-
}));
|
|
139
|
-
return {
|
|
140
|
-
content: [
|
|
141
|
-
{
|
|
142
|
-
type: 'text',
|
|
143
|
-
text: JSON.stringify({ query, components: componentResults, patterns: patternResults }, null, 2),
|
|
144
|
-
},
|
|
145
|
-
],
|
|
146
|
-
};
|
|
147
|
-
});
|
|
148
|
-
// Tool: get_composition_pattern
|
|
149
|
-
server.tool('get_composition_pattern', 'Returns a full composition pattern with structure tree, working JSX code, variants, and design notes. Query by pattern name/id or by category to get all patterns in that category.', {
|
|
150
|
-
name: z.string().optional().describe('Pattern name or id, e.g. "Profile Card" or "settings-panel"'),
|
|
151
|
-
category: z.string().optional().describe('Pattern category: "card", "form", "nav", "dashboard", "settings", or "action"'),
|
|
152
|
-
}, async ({ name, category }) => {
|
|
153
|
-
if (name) {
|
|
154
|
-
const pattern = findPattern(name);
|
|
155
|
-
if (!pattern) {
|
|
156
|
-
return {
|
|
157
|
-
content: [
|
|
158
|
-
{
|
|
159
|
-
type: 'text',
|
|
160
|
-
text: JSON.stringify({
|
|
161
|
-
error: `Pattern "${name}" not found.`,
|
|
162
|
-
available: ALL_PATTERNS.map((r) => ({ id: r.id, name: r.name, category: r.category })),
|
|
163
|
-
}),
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
isError: true,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
return {
|
|
170
|
-
content: [
|
|
171
|
-
{
|
|
172
|
-
type: 'text',
|
|
173
|
-
text: JSON.stringify(pattern, null, 2),
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
if (category) {
|
|
179
|
-
const cat = category.trim().toLowerCase();
|
|
180
|
-
const patterns = ALL_PATTERNS.filter((r) => r.category === cat);
|
|
181
|
-
if (patterns.length === 0) {
|
|
182
|
-
return {
|
|
183
|
-
content: [
|
|
184
|
-
{
|
|
185
|
-
type: 'text',
|
|
186
|
-
text: JSON.stringify({
|
|
187
|
-
error: `No patterns found in category "${category}".`,
|
|
188
|
-
availableCategories: [...new Set(ALL_PATTERNS.map((r) => r.category))],
|
|
189
|
-
}),
|
|
190
|
-
},
|
|
191
|
-
],
|
|
192
|
-
isError: true,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
content: [
|
|
197
|
-
{
|
|
198
|
-
type: 'text',
|
|
199
|
-
text: JSON.stringify({ category: cat, patterns }, null, 2),
|
|
200
|
-
},
|
|
201
|
-
],
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
// No filter — return all patterns
|
|
205
|
-
return {
|
|
206
|
-
content: [
|
|
207
|
-
{
|
|
208
|
-
type: 'text',
|
|
209
|
-
text: JSON.stringify({
|
|
210
|
-
patterns: ALL_PATTERNS.map((r) => ({
|
|
211
|
-
id: r.id,
|
|
212
|
-
name: r.name,
|
|
213
|
-
category: r.category,
|
|
214
|
-
description: r.description,
|
|
215
|
-
components: r.components,
|
|
216
|
-
})),
|
|
217
|
-
}, null, 2),
|
|
218
|
-
},
|
|
219
|
-
],
|
|
220
|
-
};
|
|
221
|
-
});
|
|
222
|
-
// Tool: list_presets
|
|
223
|
-
server.tool('list_presets', 'Lists all available Lucent UI design presets. Returns combined presets (modern, enterprise, playful) and individual dimensions (palettes, shapes, densities, shadows) that can be mixed and matched.', {}, async () => {
|
|
224
|
-
return {
|
|
225
|
-
content: [
|
|
226
|
-
{
|
|
227
|
-
type: 'text',
|
|
228
|
-
text: JSON.stringify({
|
|
229
|
-
combined: COMBINED,
|
|
230
|
-
palettes: PALETTES,
|
|
231
|
-
shapes: SHAPES,
|
|
232
|
-
densities: DENSITIES,
|
|
233
|
-
shadows: SHADOWS,
|
|
234
|
-
}, null, 2),
|
|
235
|
-
},
|
|
236
|
-
],
|
|
237
|
-
};
|
|
238
|
-
});
|
|
239
|
-
// Tool: get_preset_config
|
|
240
|
-
server.tool('get_preset_config', 'Returns the LucentProvider configuration code for a given preset selection. Pass a combined preset name OR individual dimension names to get a ready-to-use config file and provider snippet.', {
|
|
241
|
-
preset: z.string().optional().describe('Combined preset name: "modern", "enterprise", or "playful"'),
|
|
242
|
-
palette: z.string().optional().describe('Palette name: "default", "brand", "indigo", "emerald", "rose", or "ocean"'),
|
|
243
|
-
shape: z.string().optional().describe('Shape name: "sharp", "rounded", or "pill"'),
|
|
244
|
-
density: z.string().optional().describe('Density name: "compact", "default", or "spacious"'),
|
|
245
|
-
shadow: z.string().optional().describe('Shadow name: "flat", "subtle", or "elevated"'),
|
|
246
|
-
}, async ({ preset, palette, shape, density, shadow }) => {
|
|
247
|
-
const result = generatePresetConfig({ preset, palette, shape, density, shadow });
|
|
248
|
-
if ('error' in result) {
|
|
249
|
-
return {
|
|
250
|
-
content: [{ type: 'text', text: JSON.stringify({ error: result.error }) }],
|
|
251
|
-
isError: true,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
return {
|
|
255
|
-
content: [
|
|
256
|
-
{
|
|
257
|
-
type: 'text',
|
|
258
|
-
text: JSON.stringify({
|
|
259
|
-
configFile: result.configFile,
|
|
260
|
-
providerSnippet: result.providerSnippet,
|
|
261
|
-
}, null, 2),
|
|
262
|
-
},
|
|
263
|
-
],
|
|
264
|
-
};
|
|
265
|
-
});
|
|
266
|
-
// Tool: get_design_rules
|
|
267
|
-
server.tool('get_design_rules', 'Returns Lucent UI design rules for spacing, typography, button pairing, layout patterns, color usage, and density. These rules ensure AI-generated layouts are aesthetically consistent. Query a specific section or get all rules.', {
|
|
268
|
-
section: z
|
|
269
|
-
.string()
|
|
270
|
-
.optional()
|
|
271
|
-
.describe('Optional section id: "spacing", "typography", "buttons", "layout", "color", or "density". Omit to get all rules.'),
|
|
272
|
-
}, async ({ section }) => {
|
|
273
|
-
if (section) {
|
|
274
|
-
const s = section.trim().toLowerCase();
|
|
275
|
-
const rule = DESIGN_RULES.find((r) => r.id === s);
|
|
276
|
-
if (!rule) {
|
|
277
|
-
return {
|
|
278
|
-
content: [
|
|
279
|
-
{
|
|
280
|
-
type: 'text',
|
|
281
|
-
text: JSON.stringify({
|
|
282
|
-
error: `Section "${section}" not found.`,
|
|
283
|
-
availableSections: DESIGN_RULES.map((r) => ({
|
|
284
|
-
id: r.id,
|
|
285
|
-
title: r.title,
|
|
286
|
-
})),
|
|
287
|
-
}),
|
|
288
|
-
},
|
|
289
|
-
],
|
|
290
|
-
isError: true,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
return {
|
|
294
|
-
content: [
|
|
295
|
-
{
|
|
296
|
-
type: 'text',
|
|
297
|
-
text: `## ${rule.title}\n\n${rule.body}`,
|
|
298
|
-
},
|
|
299
|
-
],
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
return {
|
|
303
|
-
content: [
|
|
304
|
-
{
|
|
305
|
-
type: 'text',
|
|
306
|
-
text: DESIGN_RULES_SUMMARY,
|
|
307
|
-
},
|
|
308
|
-
],
|
|
309
|
-
};
|
|
310
|
-
});
|
|
19
|
+
registerTools(server);
|
|
311
20
|
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
312
21
|
const transport = new StdioServerTransport();
|
|
313
22
|
await server.connect(transport);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Structured logging for MCP tool calls.
|
|
4
|
+
*
|
|
5
|
+
* One JSON line per call is written to stderr (greppable, parseable by log
|
|
6
|
+
* shippers). Set `LUCENT_MCP_QUIET=1` to disable all logging.
|
|
7
|
+
*
|
|
8
|
+
* When `LUCENT_API_KEY` is set, a short hash prefix of the key is included
|
|
9
|
+
* in log entries for usage analytics. The raw key is never logged.
|
|
10
|
+
*/
|
|
11
|
+
const QUIET = process.env['LUCENT_MCP_QUIET'] === '1';
|
|
12
|
+
/**
|
|
13
|
+
* Returns the first 8 hex chars of sha256(key). Short enough to stay readable
|
|
14
|
+
* in logs, safe to leak (pre-image resistant), and unique enough to distinguish
|
|
15
|
+
* customers once multi-key auth lands (see issue #15).
|
|
16
|
+
*/
|
|
17
|
+
function hashKeyPrefix(key) {
|
|
18
|
+
return createHash('sha256').update(key).digest('hex').slice(0, 8);
|
|
19
|
+
}
|
|
20
|
+
export function logToolCall(entry) {
|
|
21
|
+
if (QUIET)
|
|
22
|
+
return;
|
|
23
|
+
const apiKey = process.env['LUCENT_API_KEY'];
|
|
24
|
+
const line = JSON.stringify({
|
|
25
|
+
t: new Date().toISOString(),
|
|
26
|
+
tool: entry.tool,
|
|
27
|
+
params: entry.params,
|
|
28
|
+
durationMs: entry.durationMs,
|
|
29
|
+
ok: entry.ok,
|
|
30
|
+
...(entry.error !== undefined && { error: entry.error }),
|
|
31
|
+
...(apiKey !== undefined && { key: hashKeyPrefix(apiKey) }),
|
|
32
|
+
});
|
|
33
|
+
process.stderr.write(line + '\n');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Wraps a tool handler with timing + structured logging. The returned function
|
|
37
|
+
* has the same signature as the input, so it can be passed directly to
|
|
38
|
+
* `server.tool(...)` without any call-site changes.
|
|
39
|
+
*/
|
|
40
|
+
export function withLogging(name, handler) {
|
|
41
|
+
const wrapped = async (...args) => {
|
|
42
|
+
const start = Date.now();
|
|
43
|
+
const params = args[0] ?? {};
|
|
44
|
+
try {
|
|
45
|
+
const result = await handler(...args);
|
|
46
|
+
logToolCall({
|
|
47
|
+
tool: name,
|
|
48
|
+
params,
|
|
49
|
+
durationMs: Date.now() - start,
|
|
50
|
+
ok: !result.isError,
|
|
51
|
+
});
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
logToolCall({
|
|
56
|
+
tool: name,
|
|
57
|
+
params,
|
|
58
|
+
durationMs: Date.now() - start,
|
|
59
|
+
ok: false,
|
|
60
|
+
error: err instanceof Error ? err.message : String(err),
|
|
61
|
+
});
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return wrapped;
|
|
66
|
+
}
|