@znt/mcp 1.1.1 → 2.0.2
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/AGENTS_EXAMPLE.md +50 -0
- package/README.md +459 -90
- package/index.js +18 -119
- package/package.json +30 -21
- package/src/credential-setup.js +726 -0
- package/src/metrics.js +121 -0
- package/src/server.js +30 -0
- package/src/tool-definitions.js +143 -0
- package/src/znt-tools.js +555 -0
package/src/metrics.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const ALTERNATIVE_MULTIPLIERS = Object.freeze({
|
|
5
|
+
search: 10,
|
|
6
|
+
similar: 10,
|
|
7
|
+
graph: 25,
|
|
8
|
+
outline: 15,
|
|
9
|
+
logs: 3,
|
|
10
|
+
status: 1,
|
|
11
|
+
scan: 1,
|
|
12
|
+
stats: 1,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export class UsageMetrics {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.configuredFile = options.file ?? process.env.ZNT_MCP_METRICS_FILE;
|
|
18
|
+
this.metricsFile = this.configuredFile ? resolve(this.configuredFile) : undefined;
|
|
19
|
+
this.sessionStartTime = new Date().toISOString();
|
|
20
|
+
this.sessionRecords = [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setProjectRoot(projectRoot) {
|
|
24
|
+
if (!this.configuredFile && projectRoot) this.metricsFile = join(projectRoot, ".znt", "mcp-usage.jsonl");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async record(toolName, input, output, executionMs) {
|
|
28
|
+
const inputTokens = estimateTokens(input);
|
|
29
|
+
const outputTokens = estimateTokens(output);
|
|
30
|
+
const estimatedAlternativeTokens = Math.ceil(outputTokens * (ALTERNATIVE_MULTIPLIERS[toolName] ?? 1));
|
|
31
|
+
const record = {
|
|
32
|
+
timestamp: new Date().toISOString(),
|
|
33
|
+
tool_name: toolName,
|
|
34
|
+
input_tokens: inputTokens,
|
|
35
|
+
output_tokens: outputTokens,
|
|
36
|
+
execution_ms: executionMs,
|
|
37
|
+
estimated_alternative_tokens: estimatedAlternativeTokens,
|
|
38
|
+
saved_tokens: Math.max(0, estimatedAlternativeTokens - outputTokens),
|
|
39
|
+
};
|
|
40
|
+
this.sessionRecords.push(record);
|
|
41
|
+
if (this.metricsFile) {
|
|
42
|
+
try {
|
|
43
|
+
await mkdir(dirname(this.metricsFile), { recursive: true });
|
|
44
|
+
await appendFile(this.metricsFile, `${JSON.stringify(record)}\n`, "utf8");
|
|
45
|
+
} catch {
|
|
46
|
+
// Metrics must never turn a successful code-intelligence call into a
|
|
47
|
+
// failed MCP tool response.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return record;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async snapshot() {
|
|
54
|
+
const persisted = await this.readPersistedRecords();
|
|
55
|
+
const session = summarize(this.sessionRecords);
|
|
56
|
+
const total = summarize(persisted.length ? persisted : this.sessionRecords);
|
|
57
|
+
return {
|
|
58
|
+
session_start_time: this.sessionStartTime,
|
|
59
|
+
session_calls: session.calls,
|
|
60
|
+
session_output_tokens: session.outputTokens,
|
|
61
|
+
session_saved_tokens: session.savedTokens,
|
|
62
|
+
last_call_saved_tokens: this.sessionRecords.at(-1)?.saved_tokens ?? 0,
|
|
63
|
+
total_calls: total.calls,
|
|
64
|
+
total_output_tokens: total.outputTokens,
|
|
65
|
+
estimated_alternative_tokens: total.alternativeTokens,
|
|
66
|
+
total_saved_tokens: total.savedTokens,
|
|
67
|
+
savings_ratio: total.outputTokens ? `${(total.alternativeTokens / total.outputTokens).toFixed(1)}x` : "N/A",
|
|
68
|
+
avg_execution_ms: Math.round(total.averageExecutionMs),
|
|
69
|
+
top_tools: summarizeTools(persisted.length ? persisted : this.sessionRecords),
|
|
70
|
+
recent_calls: (persisted.length ? persisted : this.sessionRecords).slice(-10).reverse(),
|
|
71
|
+
persistence: this.metricsFile ?? null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async readPersistedRecords() {
|
|
76
|
+
if (!this.metricsFile) return [];
|
|
77
|
+
try {
|
|
78
|
+
return (await readFile(this.metricsFile, "utf8")).split("\n").filter(Boolean).flatMap((line) => {
|
|
79
|
+
try { return [JSON.parse(line)]; } catch { return []; }
|
|
80
|
+
});
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function estimateTokens(value) {
|
|
88
|
+
const text = typeof value === "string" ? value : JSON.stringify(value ?? null);
|
|
89
|
+
return Math.ceil(text.length / 4);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function summarize(records) {
|
|
93
|
+
const execution = records.reduce((sum, record) => sum + Number(record.execution_ms || 0), 0);
|
|
94
|
+
return {
|
|
95
|
+
calls: records.length,
|
|
96
|
+
outputTokens: records.reduce((sum, record) => sum + Number(record.output_tokens || 0), 0),
|
|
97
|
+
alternativeTokens: records.reduce((sum, record) => sum + Number(record.estimated_alternative_tokens || 0), 0),
|
|
98
|
+
savedTokens: records.reduce((sum, record) => sum + Number(record.saved_tokens || 0), 0),
|
|
99
|
+
averageExecutionMs: records.length ? execution / records.length : 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function summarizeTools(records) {
|
|
104
|
+
const groups = new Map();
|
|
105
|
+
for (const record of records) {
|
|
106
|
+
const group = groups.get(record.tool_name) ?? [];
|
|
107
|
+
group.push(record);
|
|
108
|
+
groups.set(record.tool_name, group);
|
|
109
|
+
}
|
|
110
|
+
return [...groups.entries()].map(([toolName, toolRecords]) => {
|
|
111
|
+
const totals = summarize(toolRecords);
|
|
112
|
+
return {
|
|
113
|
+
tool_name: toolName,
|
|
114
|
+
calls: totals.calls,
|
|
115
|
+
total_output: totals.outputTokens,
|
|
116
|
+
total_alt: totals.alternativeTokens,
|
|
117
|
+
avg_ms: Math.round(totals.averageExecutionMs),
|
|
118
|
+
savings_ratio: totals.outputTokens ? Number((totals.alternativeTokens / totals.outputTokens).toFixed(1)) : 0,
|
|
119
|
+
};
|
|
120
|
+
}).sort((left, right) => right.calls - left.calls || left.tool_name.localeCompare(right.tool_name)).slice(0, 20);
|
|
121
|
+
}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import {
|
|
3
|
+
CallToolRequestSchema,
|
|
4
|
+
ListToolsRequestSchema,
|
|
5
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
|
|
7
|
+
import { TOOL_DEFINITIONS } from "./tool-definitions.js";
|
|
8
|
+
import { ZntTools } from "./znt-tools.js";
|
|
9
|
+
|
|
10
|
+
export function createZntMcpServer(options = {}) {
|
|
11
|
+
const tools = options.tools ?? new ZntTools(options);
|
|
12
|
+
const server = new Server(
|
|
13
|
+
{ name: "znt-mcp-server", version: "2.0.2" },
|
|
14
|
+
{ capabilities: { tools: {} } },
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFINITIONS }));
|
|
18
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => tools.call(
|
|
19
|
+
request.params.name,
|
|
20
|
+
request.params.arguments ?? {},
|
|
21
|
+
{ signal: extra?.signal },
|
|
22
|
+
));
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
server,
|
|
26
|
+
tools,
|
|
27
|
+
initialize: () => tools.initialize(),
|
|
28
|
+
disconnect: () => tools.disconnect(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const searchProperties = {
|
|
2
|
+
query: { type: "string", minLength: 1, description: "Natural-language intent or an exact code symbol to find." },
|
|
3
|
+
mode: { type: "string", enum: ["hybrid", "lexical", "vector"], default: "hybrid", description: "Use lexical for exact identifiers, hybrid for intent, and vector for conceptual similarity." },
|
|
4
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
|
|
5
|
+
callers_level: { type: "integer", minimum: 0, maximum: 10, default: 0 },
|
|
6
|
+
callees_level: { type: "integer", minimum: 0, maximum: 10, default: 0 },
|
|
7
|
+
compact: { type: "boolean", default: false, description: "Omit caller/callee graphs and return the compact result shape." },
|
|
8
|
+
include_code: { type: "boolean", default: false },
|
|
9
|
+
max_code_lines: { type: "integer", minimum: 1, maximum: 10000, default: 30 },
|
|
10
|
+
role_boost: { type: "string", minLength: 1, description: "Soft ranking boost for an architectural role." },
|
|
11
|
+
type_boost: { type: "string", minLength: 1, description: "Soft ranking boost for an AST type." },
|
|
12
|
+
file_pattern: { type: "string", minLength: 1, description: "Glob-like file path filter." },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const TOOL_DEFINITIONS = Object.freeze([
|
|
16
|
+
{
|
|
17
|
+
name: "setup",
|
|
18
|
+
description: "Run this before scanning. Invoking with no arguments opens an interactive browser setup wizard to configure providers, API keys, models, and project settings. You can also pass mode to configure directly.",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
interactive: { type: "boolean", default: true, description: "Whether to open the browser setup wizard." },
|
|
23
|
+
mode: { type: "string", enum: ["bm25", "ollama", "openapi"], description: "BM25 uses no LLM or embeddings; ollama is local; openapi supports OpenRouter and compatible providers." },
|
|
24
|
+
provider_url: { type: "string", minLength: 1 },
|
|
25
|
+
model: { type: "string", minLength: 1, description: "Generation model name." },
|
|
26
|
+
embed_model: { type: "string", minLength: 1, description: "Embedding model name, or none to disable vector search." },
|
|
27
|
+
credential_source: { type: "string", enum: ["keyring", "env"], default: "keyring" },
|
|
28
|
+
token_env: { type: "string", pattern: "^[A-Za-z_][A-Za-z0-9_]*$", description: "Environment variable name only; never put its value here." },
|
|
29
|
+
semantic_mode: { type: "string", enum: ["fast", "llm"] },
|
|
30
|
+
description_language: { type: "string", minLength: 2, maxLength: 16 },
|
|
31
|
+
exclude: { type: "array", items: { type: "string", minLength: 1 }, uniqueItems: true },
|
|
32
|
+
check_provider: { type: "boolean", default: true },
|
|
33
|
+
},
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "setup_status",
|
|
39
|
+
description: "Inspect whether Core, YAML configuration, credentials, and optionally the configured provider are ready. Never returns credential values.",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: { check_provider: { type: "boolean", default: false } },
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "search",
|
|
48
|
+
description: "Search indexed code after setup and scan. Prefer lexical for exact identifiers, hybrid for behavior, and vector for conceptual names.",
|
|
49
|
+
inputSchema: { type: "object", properties: searchProperties, required: ["query"], additionalProperties: false },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "similar",
|
|
53
|
+
description: "Find implementations, patterns, or potential duplicates similar to an indexed symbol or a text/code query. Requires embeddings.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
target: { type: "string", minLength: 1, description: "Existing symbol, qualified name, or file::symbol reference." },
|
|
58
|
+
query: { type: "string", minLength: 1, description: "Text or code used as the similarity reference or to rerank target results." },
|
|
59
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
|
|
60
|
+
include_code: { type: "boolean", default: false },
|
|
61
|
+
max_code_lines: { type: "integer", minimum: 1, maximum: 10000, default: 30 },
|
|
62
|
+
role_boost: { type: "string", minLength: 1 },
|
|
63
|
+
type_boost: { type: "string", minLength: 1 },
|
|
64
|
+
file_pattern: { type: "string", minLength: 1 },
|
|
65
|
+
edge_types: { type: "string", description: "Comma-separated contains,call,inherits,implements values." },
|
|
66
|
+
},
|
|
67
|
+
anyOf: [{ required: ["target"] }, { required: ["query"] }],
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "graph",
|
|
73
|
+
description: "Build a local dependency/call graph around `from`, or trace a path from `from` to `to`. Use qualified symbol names when a short name is ambiguous.",
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
from: { type: "string", minLength: 1 },
|
|
78
|
+
to: { type: "string", minLength: 1 },
|
|
79
|
+
depth: { type: "integer", minimum: 1, maximum: 100, default: 1 },
|
|
80
|
+
format: { type: "string", enum: ["text", "json", "mermaid"], default: "text" },
|
|
81
|
+
edge_types: { type: "string", description: "Comma-separated contains,call,inherits,implements values." },
|
|
82
|
+
},
|
|
83
|
+
required: ["from"],
|
|
84
|
+
additionalProperties: false,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "outline",
|
|
89
|
+
description: "Return the complete indexed top-level outline of a source file before reading the whole file.",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
path: { type: "string", minLength: 1, description: "Absolute or workspace-relative file path." },
|
|
94
|
+
include_code: { type: "boolean", default: false },
|
|
95
|
+
},
|
|
96
|
+
required: ["path"],
|
|
97
|
+
additionalProperties: false,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "logs",
|
|
102
|
+
description: "Read the znt-core in-memory activity log snapshot or a cursor-based delta.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
stream_id: { type: "string", minLength: 1 },
|
|
107
|
+
after_id: { type: "integer", minimum: 0 },
|
|
108
|
+
},
|
|
109
|
+
dependentRequired: { stream_id: ["after_id"], after_id: ["stream_id"] },
|
|
110
|
+
additionalProperties: false,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "status",
|
|
115
|
+
description: "Check whether znt-core already indexes the requested project or one of its ancestor directories, and return daemon, index, and scan status.",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
project_path: { type: "string", minLength: 1, description: "Optional absolute project directory or a path relative to ZNT_PROJECT_ROOT. Defaults to ZNT_PROJECT_ROOT or cwd." },
|
|
120
|
+
},
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "scan",
|
|
126
|
+
description: "Scan a project only after setup is valid. If setup_required is returned, call setup before retrying.",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {
|
|
130
|
+
project_path: { type: "string", minLength: 1, description: "Optional absolute project directory or a path relative to ZNT_PROJECT_ROOT. Defaults to ZNT_PROJECT_ROOT or cwd." },
|
|
131
|
+
language: { type: "string", minLength: 1, default: "auto", description: "Parser name from info.languages, or auto." },
|
|
132
|
+
},
|
|
133
|
+
additionalProperties: false,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "stats",
|
|
138
|
+
description: "Return session and persisted usage estimates for this Znt MCP server.",
|
|
139
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
export const TOOL_NAMES = new Set(TOOL_DEFINITIONS.map((tool) => tool.name));
|