ucn 5.1.1 → 5.2.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/.claude/skills/ucn/SKILL.md +26 -5
- package/.claude/skills/ucn/references/commands.md +5 -5
- package/README.md +42 -15
- package/core/accessors.js +183 -0
- package/core/analysis.js +46 -0
- package/core/ast-analysis.js +104 -0
- package/core/cache.js +32 -1
- package/core/callers.js +1255 -64
- package/core/command-contracts.js +13 -13
- package/core/deadcode.js +41 -2
- package/core/execute.js +4 -1
- package/core/graph-build.js +6 -0
- package/core/graph.js +72 -5
- package/core/index-ir.js +13 -2
- package/core/ir.js +5 -3
- package/core/output/analysis.js +30 -1
- package/core/output/graph.js +28 -8
- package/core/output/public.js +4 -0
- package/core/output/refactoring.js +31 -2
- package/core/output/reporting.js +7 -0
- package/core/project.js +32 -0
- package/core/search.js +9 -0
- package/core/verify.js +1168 -38
- package/languages/c-family.js +239 -26
- package/languages/csharp.js +40 -4
- package/languages/go.js +473 -71
- package/languages/javascript.js +86 -4
- package/languages/python.js +392 -101
- package/languages/rust.js +87 -4
- package/languages/utils.js +11 -0
- package/mcp/server.js +99 -103
- package/mcp/stdio-server.js +296 -0
- package/package.json +10 -8
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal MCP stdio transport for UCN's one-tool, local-only server.
|
|
5
|
+
*
|
|
6
|
+
* UCN does not expose HTTP, OAuth, resources, prompts, sampling, or remote
|
|
7
|
+
* transports. Pulling those facilities into every npm install substantially
|
|
8
|
+
* widened the production dependency and capability surface. This adapter
|
|
9
|
+
* implements the MCP base lifecycle plus tools/list, tools/call, and ping over
|
|
10
|
+
* newline-delimited JSON-RPC, which is the complete surface UCN advertises.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const MAX_MESSAGE_CHARS = 10 * 1024 * 1024;
|
|
14
|
+
const LATEST_PROTOCOL_VERSION = '2025-11-25';
|
|
15
|
+
const SUPPORTED_PROTOCOL_VERSIONS = new Set([
|
|
16
|
+
'2024-11-05',
|
|
17
|
+
'2025-03-26',
|
|
18
|
+
'2025-06-18',
|
|
19
|
+
LATEST_PROTOCOL_VERSION,
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const ERROR = Object.freeze({
|
|
23
|
+
PARSE: -32700,
|
|
24
|
+
INVALID_REQUEST: -32600,
|
|
25
|
+
METHOD_NOT_FOUND: -32601,
|
|
26
|
+
INVALID_PARAMS: -32602,
|
|
27
|
+
INTERNAL: -32603,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function isObject(value) {
|
|
31
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isRequestId(value) {
|
|
35
|
+
return value === null || typeof value === 'string' ||
|
|
36
|
+
(typeof value === 'number' && Number.isFinite(value));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toolError(message) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateValue(key, value, rule) {
|
|
47
|
+
if (rule.type === 'string' && typeof value !== 'string') {
|
|
48
|
+
return `${key} must be a string.`;
|
|
49
|
+
}
|
|
50
|
+
if (rule.type === 'boolean' && typeof value !== 'boolean') {
|
|
51
|
+
return `${key} must be a boolean.`;
|
|
52
|
+
}
|
|
53
|
+
if (rule.type === 'number' && (typeof value !== 'number' || !Number.isFinite(value))) {
|
|
54
|
+
return `${key} must be a finite number.`;
|
|
55
|
+
}
|
|
56
|
+
if (rule.type === 'integer' &&
|
|
57
|
+
(typeof value !== 'number' || !Number.isSafeInteger(value))) {
|
|
58
|
+
return `${key} must be an integer.`;
|
|
59
|
+
}
|
|
60
|
+
if (rule.minLength !== undefined && value.length < rule.minLength) {
|
|
61
|
+
return `${key} must contain at least ${rule.minLength} character(s).`;
|
|
62
|
+
}
|
|
63
|
+
if (rule.minimum !== undefined && value < rule.minimum) {
|
|
64
|
+
return `${key} must be greater than or equal to ${rule.minimum}.`;
|
|
65
|
+
}
|
|
66
|
+
if (rule.exclusiveMinimum !== undefined && value <= rule.exclusiveMinimum) {
|
|
67
|
+
return `${key} must be greater than ${rule.exclusiveMinimum}.`;
|
|
68
|
+
}
|
|
69
|
+
if (rule.maximum !== undefined && value > rule.maximum) {
|
|
70
|
+
return `${key} must be less than or equal to ${rule.maximum}.`;
|
|
71
|
+
}
|
|
72
|
+
if (rule.enum && key !== 'command' && !rule.enum.includes(value)) {
|
|
73
|
+
return `${key} must be one of: ${rule.enum.join(', ')}.`;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate advertised input constraints without silently stripping unknown
|
|
80
|
+
* keys. Unknown keys deliberately reach UCN's handler, which returns typo and
|
|
81
|
+
* applicability guidance to the agent. The command enum is advertised for
|
|
82
|
+
* discovery but runtime validation remains string-based so retired command
|
|
83
|
+
* names can receive directive migration guidance.
|
|
84
|
+
*/
|
|
85
|
+
function validateToolArguments(value, schema) {
|
|
86
|
+
if (!isObject(value)) return 'arguments must be an object.';
|
|
87
|
+
for (const key of schema.required || []) {
|
|
88
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
89
|
+
return `${key} is required.`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (typeof value.project_dir === 'string' && value.project_dir.trim().length === 0) {
|
|
93
|
+
return 'project_dir is required and must be a non-empty path.';
|
|
94
|
+
}
|
|
95
|
+
for (const [key, input] of Object.entries(value)) {
|
|
96
|
+
const rule = schema.properties?.[key];
|
|
97
|
+
if (!rule) continue;
|
|
98
|
+
const error = validateValue(key, input, rule);
|
|
99
|
+
if (error) return error;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class StdioMcpServer {
|
|
105
|
+
constructor(serverInfo) {
|
|
106
|
+
this.serverInfo = { ...serverInfo };
|
|
107
|
+
this.tools = new Map();
|
|
108
|
+
this.initialized = false;
|
|
109
|
+
this.inputBuffer = '';
|
|
110
|
+
this.discardOversizedLine = false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
registerTool(name, definition, handler) {
|
|
114
|
+
if (this.tools.has(name)) throw new Error(`Tool already registered: ${name}`);
|
|
115
|
+
this.tools.set(name, {
|
|
116
|
+
definition: {
|
|
117
|
+
name,
|
|
118
|
+
description: definition.description,
|
|
119
|
+
inputSchema: definition.inputSchema,
|
|
120
|
+
annotations: definition.annotations,
|
|
121
|
+
execution: { taskSupport: 'forbidden' },
|
|
122
|
+
},
|
|
123
|
+
handler,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
connect() {
|
|
128
|
+
process.stdin.setEncoding('utf8');
|
|
129
|
+
process.stdin.on('data', chunk => this._receive(chunk));
|
|
130
|
+
process.stdin.on('end', () => {
|
|
131
|
+
if (this.inputBuffer.trim()) this._consumeLine(this.inputBuffer);
|
|
132
|
+
this.inputBuffer = '';
|
|
133
|
+
});
|
|
134
|
+
process.stdin.resume();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_receive(chunk) {
|
|
138
|
+
let remaining = chunk;
|
|
139
|
+
while (remaining.length > 0) {
|
|
140
|
+
const newline = remaining.indexOf('\n');
|
|
141
|
+
const part = newline === -1 ? remaining : remaining.slice(0, newline);
|
|
142
|
+
remaining = newline === -1 ? '' : remaining.slice(newline + 1);
|
|
143
|
+
|
|
144
|
+
if (!this.discardOversizedLine) {
|
|
145
|
+
this.inputBuffer += part;
|
|
146
|
+
if (this.inputBuffer.length > MAX_MESSAGE_CHARS) {
|
|
147
|
+
this.inputBuffer = '';
|
|
148
|
+
this.discardOversizedLine = true;
|
|
149
|
+
this._sendError(null, ERROR.INVALID_REQUEST,
|
|
150
|
+
`MCP message exceeds ${MAX_MESSAGE_CHARS} characters.`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (newline !== -1) {
|
|
154
|
+
if (!this.discardOversizedLine) this._consumeLine(this.inputBuffer);
|
|
155
|
+
this.inputBuffer = '';
|
|
156
|
+
this.discardOversizedLine = false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_consumeLine(rawLine) {
|
|
162
|
+
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
|
|
163
|
+
if (!line.trim()) return;
|
|
164
|
+
let message;
|
|
165
|
+
try {
|
|
166
|
+
message = JSON.parse(line);
|
|
167
|
+
} catch (_) {
|
|
168
|
+
this._sendError(null, ERROR.PARSE, 'Parse error');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
void this._handleMessage(message);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async _handleMessage(message) {
|
|
175
|
+
if (!isObject(message) || message.jsonrpc !== '2.0' ||
|
|
176
|
+
typeof message.method !== 'string') {
|
|
177
|
+
const id = isObject(message) && isRequestId(message.id)
|
|
178
|
+
? message.id : null;
|
|
179
|
+
this._sendError(id, ERROR.INVALID_REQUEST, 'Invalid Request');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const hasId = Object.prototype.hasOwnProperty.call(message, 'id');
|
|
184
|
+
if (hasId && !isRequestId(message.id)) {
|
|
185
|
+
this._sendError(null, ERROR.INVALID_REQUEST, 'Invalid Request');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const isRequest = hasId;
|
|
189
|
+
if (!isRequest) {
|
|
190
|
+
if (message.method === 'notifications/initialized') this.initialized = true;
|
|
191
|
+
// Cancellation may be ignored when synchronous work cannot be
|
|
192
|
+
// interrupted; unknown notifications are also fire-and-forget.
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
switch (message.method) {
|
|
198
|
+
case 'initialize':
|
|
199
|
+
this._initialize(message.id, message.params);
|
|
200
|
+
return;
|
|
201
|
+
case 'ping':
|
|
202
|
+
this._sendResult(message.id, {});
|
|
203
|
+
return;
|
|
204
|
+
case 'tools/list':
|
|
205
|
+
this._listTools(message.id, message.params);
|
|
206
|
+
return;
|
|
207
|
+
case 'tools/call':
|
|
208
|
+
await this._callTool(message.id, message.params);
|
|
209
|
+
return;
|
|
210
|
+
default:
|
|
211
|
+
this._sendError(message.id, ERROR.METHOD_NOT_FOUND,
|
|
212
|
+
`Method not found: ${message.method}`);
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
this._sendError(message.id, ERROR.INTERNAL,
|
|
216
|
+
error?.message || 'Internal error');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
_initialize(id, params) {
|
|
221
|
+
if (!isObject(params) || typeof params.protocolVersion !== 'string' ||
|
|
222
|
+
!isObject(params.capabilities) || !isObject(params.clientInfo) ||
|
|
223
|
+
typeof params.clientInfo.name !== 'string' ||
|
|
224
|
+
typeof params.clientInfo.version !== 'string') {
|
|
225
|
+
this._sendError(id, ERROR.INVALID_PARAMS, 'Invalid initialize parameters.');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.has(params.protocolVersion)
|
|
229
|
+
? params.protocolVersion : LATEST_PROTOCOL_VERSION;
|
|
230
|
+
this._sendResult(id, {
|
|
231
|
+
protocolVersion,
|
|
232
|
+
capabilities: { tools: { listChanged: false } },
|
|
233
|
+
serverInfo: this.serverInfo,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
_listTools(id, params) {
|
|
238
|
+
if (params !== undefined && !isObject(params)) {
|
|
239
|
+
this._sendError(id, ERROR.INVALID_PARAMS, 'tools/list params must be an object.');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (params?.cursor !== undefined) {
|
|
243
|
+
this._sendError(id, ERROR.INVALID_PARAMS,
|
|
244
|
+
'Invalid tools/list cursor: UCN exposes one deterministic page.');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this._sendResult(id, {
|
|
248
|
+
tools: [...this.tools.values()].map(tool => tool.definition),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async _callTool(id, params) {
|
|
253
|
+
if (!isObject(params) || typeof params.name !== 'string') {
|
|
254
|
+
this._sendError(id, ERROR.INVALID_PARAMS,
|
|
255
|
+
'tools/call requires a string name and object arguments.');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const tool = this.tools.get(params.name);
|
|
259
|
+
if (!tool) {
|
|
260
|
+
this._sendError(id, ERROR.INVALID_PARAMS, `Unknown tool: ${params.name}`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const validationError = validateToolArguments(params.arguments, tool.definition.inputSchema);
|
|
264
|
+
if (validationError) {
|
|
265
|
+
this._sendResult(id, toolError(`Input validation error: ${validationError}`));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const result = await tool.handler(params.arguments);
|
|
270
|
+
this._sendResult(id, result);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
this._sendResult(id, toolError(error?.message || 'Tool execution failed.'));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_sendResult(id, result) {
|
|
277
|
+
this._write({ jsonrpc: '2.0', id, result });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_sendError(id, code, message) {
|
|
281
|
+
this._write({ jsonrpc: '2.0', id, error: { code, message } });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
_write(message) {
|
|
285
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = {
|
|
290
|
+
ERROR,
|
|
291
|
+
LATEST_PROTOCOL_VERSION,
|
|
292
|
+
MAX_MESSAGE_CHARS,
|
|
293
|
+
StdioMcpServer,
|
|
294
|
+
SUPPORTED_PROTOCOL_VERSIONS,
|
|
295
|
+
validateToolArguments,
|
|
296
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.1",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Auditable AST code intelligence for AI agents: 18 task-oriented commands through one MCP tool, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java, C, C++, C#, and HTML.",
|
|
6
6
|
"main": "index.js",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"version": "node scripts/sync-server-version.js && git add server.json",
|
|
13
|
-
"test": "node --test test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-c-family.test.js test/regression-cross.test.js test/regression-mcp.test.js test/regression-parser.test.js test/regression-commands.test.js test/regression-fixes.test.js test/regression-bugfixes.test.js test/release-surface-regressions.test.js test/prerelease-audit.test.js test/release-readiness-audit.test.js test/cross-language.test.js test/accuracy.test.js test/command-coverage.test.js test/perf-optimizations.test.js test/performance-gate-policy.test.js test/oracle-gate-policy.test.js test/consistency-eval.test.js test/agent-public-surface-benchmark.test.js test/command-contracts.test.js test/language-adapter.test.js test/systematic-test.js test/mcp-edge-cases.js test/conservation.test.js test/parity-test.js test/trust-matrix.test.js",
|
|
13
|
+
"test": "node --test test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-c-family.test.js test/regression-cross.test.js test/regression-mcp.test.js test/mcp-protocol.test.js test/mcp-sdk-compat.test.js test/dependency-security.test.js test/regression-parser.test.js test/regression-commands.test.js test/regression-fixes.test.js test/regression-bugfixes.test.js test/release-surface-regressions.test.js test/prerelease-audit.test.js test/release-readiness-audit.test.js test/cross-language.test.js test/accuracy.test.js test/command-coverage.test.js test/perf-optimizations.test.js test/performance-gate-policy.test.js test/oracle-gate-policy.test.js test/outcome-policy.test.js test/consistency-eval.test.js test/agent-public-surface-benchmark.test.js test/command-contracts.test.js test/language-adapter.test.js test/systematic-test.js test/mcp-edge-cases.js test/conservation.test.js test/parity-test.js test/trust-matrix.test.js",
|
|
14
14
|
"benchmark:agent": "node test/agent-public-surface-benchmark.js",
|
|
15
15
|
"benchmark:agent:gate": "node test/agent-public-surface-benchmark.js --gate",
|
|
16
16
|
"benchmark:agent:legacy": "node test/agent-understanding-benchmark.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"eval:host-calibration": "node eval/lib/host-calibration.js --pin",
|
|
22
22
|
"eval:consistency": "node eval/run-consistency-eval.js --project .",
|
|
23
23
|
"profile:callers": "node eval/profile-callers.js",
|
|
24
|
-
"lint": "eslint core/ cli/ mcp/ languages/ eval/run-*.js eval/*-gate-policy.js eval/profile-callers.js eval/oracles/*.js",
|
|
24
|
+
"lint": "eslint core/ cli/ mcp/ languages/ eval/run-*.js eval/*-gate-policy.js eval/outcome-policy.js eval/profile-callers.js eval/oracles/*.js",
|
|
25
25
|
"verify": "npm run lint && npm test",
|
|
26
26
|
"trust:gate:semantic": "node eval/run-oracle-eval.js --release --min-precision 0.98 --max-unscored-ratio 0.10",
|
|
27
27
|
"trust:gate:deadcode": "node eval/run-deadcode-eval.js --release --sample 100 --arm default",
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
"trust:gate:consistency": "node eval/run-consistency-eval.js --release --sample 40",
|
|
31
31
|
"trust:gate:agent": "node test/agent-public-surface-benchmark.js --gate",
|
|
32
32
|
"trust:gate:fast": "node --expose-gc eval/run-performance-gate.js --repo preact-signals,httpx --queries 20 && node eval/run-oracle-eval.js --repo preact-signals,httpx --min-precision 0.98 && node eval/run-consistency-eval.js --repo preact-signals,httpx --sample 20 --gate",
|
|
33
|
-
"trust:gate": "npm run trust:gate:performance && npm run trust:gate:semantic && npm run trust:gate:deadcode && npm run trust:gate:consistency && npm run trust:gate:agent"
|
|
33
|
+
"trust:gate": "npm run trust:gate:performance && npm run trust:gate:semantic && npm run trust:gate:deadcode && npm run trust:gate:consistency && npm run trust:gate:agent",
|
|
34
|
+
"eval:outcome": "node eval/run-outcome-eval.js",
|
|
35
|
+
"eval:census": "node eval/run-unverified-census.js"
|
|
34
36
|
},
|
|
35
37
|
"keywords": [
|
|
36
38
|
"mcp",
|
|
@@ -83,7 +85,10 @@
|
|
|
83
85
|
"node": ">=20"
|
|
84
86
|
},
|
|
85
87
|
"dependencies": {
|
|
88
|
+
"node-addon-api": "8.9.2",
|
|
89
|
+
"node-gyp-build": "4.8.4",
|
|
86
90
|
"re2js": "2.8.6",
|
|
91
|
+
"ret": "0.5.0",
|
|
87
92
|
"safe-regex2": "5.1.1",
|
|
88
93
|
"tree-sitter": "0.21.1",
|
|
89
94
|
"tree-sitter-c": "0.23.2",
|
|
@@ -97,11 +102,8 @@
|
|
|
97
102
|
"tree-sitter-rust": "0.23.1",
|
|
98
103
|
"tree-sitter-typescript": "0.23.2"
|
|
99
104
|
},
|
|
100
|
-
"optionalDependencies": {
|
|
101
|
-
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
102
|
-
"zod": "^4.4.3"
|
|
103
|
-
},
|
|
104
105
|
"devDependencies": {
|
|
106
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
105
107
|
"@eslint/js": "^10.0.1",
|
|
106
108
|
"eslint": "^10.8.1",
|
|
107
109
|
"pyright": "1.1.411",
|