@harivatsa/sirius-mcp-proxy 0.1.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/README.md +19 -0
- package/dist/enrich.js +168 -0
- package/dist/http.js +141 -0
- package/dist/impact.js +76 -0
- package/dist/index.js +314 -0
- package/dist/search.js +24 -0
- package/dist/types.js +1 -0
- package/dist/validation.js +19 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @harivatsa/sirius-mcp-proxy
|
|
2
|
+
|
|
3
|
+
MCP server that connects Claude to the Sirius index server and local repo-agent snippets.
|
|
4
|
+
|
|
5
|
+
Install from npm with:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @harivatsa/sirius-mcp-proxy
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Point Claude at `sirius-mcp-proxy` after configuring the Sirius index URL, repo-agent URL, and token environment variables described in the root README.
|
|
12
|
+
|
|
13
|
+
Snippet enrichment is bounded by `SIRIUS_MCP_ENRICH_CONCURRENCY`, which defaults to `16` and caps at `50`, matching the MCP tool `top_k` limit. Normal `top_k <= 16` searches fetch snippets in one wave; larger windows avoid unbounded local file-range fan-out.
|
|
14
|
+
|
|
15
|
+
`semantic_code_search`, `hybrid_search`, `symbol_search`, `get_call_chain`, and `impact_analysis` compare indexed file hashes with local snippet hashes when `include_code=true`. With `auto_sync_stale=true`, stale snippets trigger one local repo-agent sync and then rerun the lookup.
|
|
16
|
+
|
|
17
|
+
`get_call_chain` forwards `max_edges`, defaulting to `300`, and includes `edge_summary` when dense graph neighborhoods omit edges. It also forwards `max_unresolved`, defaulting to `200`; set it to `0` for compact graph-only responses when unresolved callees are not needed. `max_code_nodes` defaults to `50`, so large graph and impact-analysis responses keep all node metadata but only enrich the first N nodes with local source snippets.
|
|
18
|
+
|
|
19
|
+
The package is published publicly on npm as `@harivatsa/sirius-mcp-proxy`.
|
package/dist/enrich.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { agentPost } from "./http.js";
|
|
2
|
+
import { isSafeLocalFilePath, LOCAL_FILE_PATH_RULE } from "./validation.js";
|
|
3
|
+
// General rule: snippet enrichment should not fan out unbounded local file reads for large top_k calls.
|
|
4
|
+
const DEFAULT_MCP_ENRICH_CONCURRENCY = 16;
|
|
5
|
+
// General rule: callers may tune enrichment parallelism, but it should remain bounded by MCP tool limits.
|
|
6
|
+
const MAX_MCP_ENRICH_CONCURRENCY = 50;
|
|
7
|
+
// General rule: one snippet batch should fit the largest current MCP graph response without unbounded payloads.
|
|
8
|
+
const MAX_MCP_SNIPPET_BATCH_SIZE = 200;
|
|
9
|
+
// General rule: indexed file paths must be normalized repo-relative slash paths before local file lookup.
|
|
10
|
+
const UNSAFE_SNIPPET_PATH_ERROR = LOCAL_FILE_PATH_RULE;
|
|
11
|
+
// General rule: local snippet reads should be bound to the repo requested by the MCP tool call, not by index payloads.
|
|
12
|
+
const MISMATCHED_SNIPPET_REPO_ERROR = "indexed result repo_id does not match requested repo_id";
|
|
13
|
+
// General rule: local agent snippets must echo the requested repo and file before MCP returns source text.
|
|
14
|
+
const MISMATCHED_LOCAL_SNIPPET_ERROR = "local snippet response does not match requested repo_id or file_path";
|
|
15
|
+
export async function enrichSearchResults(repoId, results, includeCode) {
|
|
16
|
+
return enrichWithSnippets(results, includeCode, (result) => ({
|
|
17
|
+
repoId,
|
|
18
|
+
indexedRepoId: result.repo_id,
|
|
19
|
+
filePath: result.file_path,
|
|
20
|
+
startLine: result.start_line,
|
|
21
|
+
endLine: result.end_line,
|
|
22
|
+
expectedFileHash: result.file_hash
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
export async function enrichSymbols(repoId, results, includeCode) {
|
|
26
|
+
return enrichWithSnippets(results, includeCode, (result) => ({
|
|
27
|
+
repoId,
|
|
28
|
+
indexedRepoId: result.repo_id,
|
|
29
|
+
filePath: result.file_path,
|
|
30
|
+
startLine: result.start_line,
|
|
31
|
+
endLine: result.end_line,
|
|
32
|
+
expectedFileHash: result.file_hash ?? undefined
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
export async function enrichGraphNodes(repoId, nodes, includeCode, maxSnippetNodes = nodes.length) {
|
|
36
|
+
const snippetNodeCount = boundedItemCount(maxSnippetNodes, nodes.length);
|
|
37
|
+
const nodesWithSnippets = await enrichWithSnippets(nodes.slice(0, snippetNodeCount), includeCode, (node) => ({
|
|
38
|
+
repoId,
|
|
39
|
+
indexedRepoId: node.repo_id,
|
|
40
|
+
filePath: node.file_path,
|
|
41
|
+
startLine: node.start_line,
|
|
42
|
+
endLine: node.end_line,
|
|
43
|
+
expectedFileHash: node.file_hash ?? undefined
|
|
44
|
+
}));
|
|
45
|
+
return [...nodesWithSnippets, ...nodes.slice(snippetNodeCount)];
|
|
46
|
+
}
|
|
47
|
+
async function enrichWithSnippets(items, includeCode, lookupFor) {
|
|
48
|
+
if (!includeCode) {
|
|
49
|
+
return items;
|
|
50
|
+
}
|
|
51
|
+
const lookups = items.map(lookupFor);
|
|
52
|
+
const snippets = await getSnippetLookups(lookups);
|
|
53
|
+
return items.map((item, index) => {
|
|
54
|
+
const snippet = snippets[index]?.snippet;
|
|
55
|
+
if (snippet) {
|
|
56
|
+
return { ...item, snippet };
|
|
57
|
+
}
|
|
58
|
+
return { ...item, snippet_error: snippets[index]?.error ?? "local snippet lookup failed" };
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function getSnippetLookups(lookups) {
|
|
62
|
+
const results = new Array(lookups.length);
|
|
63
|
+
const safeIndexes = [];
|
|
64
|
+
lookups.forEach((lookup, index) => {
|
|
65
|
+
if (lookup.indexedRepoId !== undefined && lookup.indexedRepoId !== lookup.repoId) {
|
|
66
|
+
results[index] = { error: MISMATCHED_SNIPPET_REPO_ERROR };
|
|
67
|
+
}
|
|
68
|
+
else if (isSafeLocalFilePath(lookup.filePath)) {
|
|
69
|
+
safeIndexes.push(index);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
results[index] = { error: UNSAFE_SNIPPET_PATH_ERROR };
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const batches = snippetBatches(lookups, safeIndexes);
|
|
76
|
+
await mapWithConcurrency(batches, mcpEnrichConcurrency(), async (batch) => {
|
|
77
|
+
try {
|
|
78
|
+
const response = await agentPost(`/repos/${encodeURIComponent(batch.repoId)}/file-ranges`, {
|
|
79
|
+
ranges: batch.indexes.map((index) => ({
|
|
80
|
+
file_path: lookups[index].filePath,
|
|
81
|
+
start_line: lookups[index].startLine,
|
|
82
|
+
end_line: lookups[index].endLine
|
|
83
|
+
}))
|
|
84
|
+
});
|
|
85
|
+
if (!Array.isArray(response.results) || response.results.length !== batch.indexes.length) {
|
|
86
|
+
throw new Error("local repo agent returned an invalid file-ranges response");
|
|
87
|
+
}
|
|
88
|
+
if (response.repo_id !== batch.repoId) {
|
|
89
|
+
throw new Error(MISMATCHED_LOCAL_SNIPPET_ERROR);
|
|
90
|
+
}
|
|
91
|
+
response.results.forEach((item, offset) => {
|
|
92
|
+
const index = batch.indexes[offset];
|
|
93
|
+
const lookup = lookups[index];
|
|
94
|
+
if (item.ok) {
|
|
95
|
+
if (item.range.repo_id !== batch.repoId || item.range.file_path !== lookup.filePath || !isSafeLocalFilePath(item.range.file_path)) {
|
|
96
|
+
results[index] = { error: MISMATCHED_LOCAL_SNIPPET_ERROR };
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
results[index] = {
|
|
100
|
+
snippet: {
|
|
101
|
+
...item.range,
|
|
102
|
+
expected_file_hash: lookup.expectedFileHash,
|
|
103
|
+
stale: lookup.expectedFileHash === undefined ? false : item.range.file_hash !== lookup.expectedFileHash
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
results[index] = { error: item.error ?? "local snippet lookup failed" };
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
for (const index of batch.indexes) {
|
|
114
|
+
results[index] = { error: error.message };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
return results;
|
|
119
|
+
}
|
|
120
|
+
function snippetBatches(lookups, lookupIndexes) {
|
|
121
|
+
const grouped = new Map();
|
|
122
|
+
lookupIndexes.forEach((index) => {
|
|
123
|
+
const lookup = lookups[index];
|
|
124
|
+
const indexes = grouped.get(lookup.repoId);
|
|
125
|
+
if (indexes) {
|
|
126
|
+
indexes.push(index);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
grouped.set(lookup.repoId, [index]);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
const batches = [];
|
|
133
|
+
for (const [repoId, indexes] of grouped) {
|
|
134
|
+
for (let start = 0; start < indexes.length; start += MAX_MCP_SNIPPET_BATCH_SIZE) {
|
|
135
|
+
batches.push({ repoId, indexes: indexes.slice(start, start + MAX_MCP_SNIPPET_BATCH_SIZE) });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return batches;
|
|
139
|
+
}
|
|
140
|
+
export function mcpEnrichConcurrency() {
|
|
141
|
+
const configured = Number(process.env.SIRIUS_MCP_ENRICH_CONCURRENCY ?? String(DEFAULT_MCP_ENRICH_CONCURRENCY));
|
|
142
|
+
if (!Number.isFinite(configured) || configured < 1) {
|
|
143
|
+
return DEFAULT_MCP_ENRICH_CONCURRENCY;
|
|
144
|
+
}
|
|
145
|
+
return Math.min(Math.floor(configured), MAX_MCP_ENRICH_CONCURRENCY);
|
|
146
|
+
}
|
|
147
|
+
function boundedItemCount(value, max) {
|
|
148
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
return Math.min(Math.floor(value), max);
|
|
152
|
+
}
|
|
153
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
154
|
+
if (items.length === 0) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
const results = new Array(items.length);
|
|
158
|
+
let nextIndex = 0;
|
|
159
|
+
const workerCount = Math.min(concurrency, items.length);
|
|
160
|
+
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
161
|
+
while (nextIndex < items.length) {
|
|
162
|
+
const index = nextIndex;
|
|
163
|
+
nextIndex += 1;
|
|
164
|
+
results[index] = await mapper(items[index], index);
|
|
165
|
+
}
|
|
166
|
+
}));
|
|
167
|
+
return results;
|
|
168
|
+
}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
export const INDEX_URL = trimSlash(process.env.SIRIUS_INDEX_URL ?? "http://127.0.0.1:8000");
|
|
3
|
+
export const AGENT_URL = trimSlash(process.env.SIRIUS_AGENT_URL ?? "http://127.0.0.1:3987");
|
|
4
|
+
// General rule: MCP tool calls should fail fast enough for callers to recover from stalled index/agent HTTP paths.
|
|
5
|
+
const DEFAULT_MCP_HTTP_TIMEOUT_MS = 30_000;
|
|
6
|
+
// General rule: timeout overrides should remain bounded so a wedged dependency cannot pin the MCP process forever.
|
|
7
|
+
const MAX_MCP_HTTP_TIMEOUT_MS = 300_000;
|
|
8
|
+
// General rule: upstream error bodies may echo request credentials, so MCP-visible HTTP errors must redact them.
|
|
9
|
+
const API_TOKEN_REDACTION_TEXT = "[redacted-token]";
|
|
10
|
+
// General rule: local repo-agent calls should carry a non-CORS-simple header that browser forms/images cannot send.
|
|
11
|
+
const AGENT_CLIENT_HEADER = "x-sirius-agent-client";
|
|
12
|
+
const AGENT_CLIENT_HEADER_VALUE = "sirius";
|
|
13
|
+
export async function indexGet(route) {
|
|
14
|
+
return requestJson(`${INDEX_URL}${route}`, { token: await resolveIndexApiToken() });
|
|
15
|
+
}
|
|
16
|
+
export async function indexPost(route, body) {
|
|
17
|
+
return requestJson(`${INDEX_URL}${route}`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
token: await resolveIndexApiToken(),
|
|
20
|
+
body
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export async function indexDelete(route) {
|
|
24
|
+
return requestJson(`${INDEX_URL}${route}`, {
|
|
25
|
+
method: "DELETE",
|
|
26
|
+
token: await resolveIndexApiToken()
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export async function agentGet(route) {
|
|
30
|
+
return requestJson(`${AGENT_URL}${route}`, { agentClient: true });
|
|
31
|
+
}
|
|
32
|
+
export async function agentPost(route, body) {
|
|
33
|
+
return requestJson(`${AGENT_URL}${route}`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
body,
|
|
36
|
+
agentClient: true
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async function requestJson(url, options = {}) {
|
|
40
|
+
const headers = {};
|
|
41
|
+
let body;
|
|
42
|
+
if (options.body !== undefined) {
|
|
43
|
+
headers["content-type"] = "application/json";
|
|
44
|
+
body = JSON.stringify(options.body);
|
|
45
|
+
}
|
|
46
|
+
if (options.token) {
|
|
47
|
+
headers.authorization = `Bearer ${options.token}`;
|
|
48
|
+
}
|
|
49
|
+
if (options.agentClient) {
|
|
50
|
+
headers[AGENT_CLIENT_HEADER] = AGENT_CLIENT_HEADER_VALUE;
|
|
51
|
+
}
|
|
52
|
+
const timeoutMs = mcpHttpTimeoutMs();
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
let timedOut = false;
|
|
55
|
+
const timeout = setTimeout(() => {
|
|
56
|
+
timedOut = true;
|
|
57
|
+
controller.abort();
|
|
58
|
+
}, timeoutMs);
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(url, {
|
|
61
|
+
method: options.method ?? "GET",
|
|
62
|
+
headers,
|
|
63
|
+
body,
|
|
64
|
+
signal: controller.signal
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const statusText = redactApiToken(response.statusText, options.token);
|
|
68
|
+
const text = redactApiToken(await response.text(), options.token);
|
|
69
|
+
throw new Error(`${response.status} ${statusText}: ${text}`);
|
|
70
|
+
}
|
|
71
|
+
if (response.status === 204) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
return (await response.json());
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (timedOut) {
|
|
78
|
+
throw new Error(`Sirius MCP HTTP request timed out after ${timeoutMs} ms: ${url}`);
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function redactApiToken(text, token) {
|
|
87
|
+
const trimmed = token?.trim();
|
|
88
|
+
if (!trimmed) {
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
return text
|
|
92
|
+
.split(`Bearer ${trimmed}`)
|
|
93
|
+
.join(`Bearer ${API_TOKEN_REDACTION_TEXT}`)
|
|
94
|
+
.split(trimmed)
|
|
95
|
+
.join(API_TOKEN_REDACTION_TEXT);
|
|
96
|
+
}
|
|
97
|
+
export async function resolveIndexApiToken() {
|
|
98
|
+
const configuredToken = firstNonEmpty(process.env.SIRIUS_API_TOKEN);
|
|
99
|
+
if (configuredToken) {
|
|
100
|
+
return configuredToken;
|
|
101
|
+
}
|
|
102
|
+
const configuredTokenFile = firstNonEmpty(process.env.SIRIUS_API_TOKEN_FILE);
|
|
103
|
+
if (!configuredTokenFile) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return readApiTokenFile(configuredTokenFile);
|
|
107
|
+
}
|
|
108
|
+
async function readApiTokenFile(tokenFile) {
|
|
109
|
+
let raw;
|
|
110
|
+
try {
|
|
111
|
+
raw = await fs.readFile(tokenFile, "utf8");
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new Error("Sirius API token file from SIRIUS_API_TOKEN_FILE is not readable");
|
|
115
|
+
}
|
|
116
|
+
const token = raw.trim();
|
|
117
|
+
if (!token) {
|
|
118
|
+
throw new Error("Sirius API token file from SIRIUS_API_TOKEN_FILE is empty");
|
|
119
|
+
}
|
|
120
|
+
return token;
|
|
121
|
+
}
|
|
122
|
+
function firstNonEmpty(...values) {
|
|
123
|
+
for (const value of values) {
|
|
124
|
+
const trimmed = value?.trim();
|
|
125
|
+
if (trimmed) {
|
|
126
|
+
return trimmed;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
function mcpHttpTimeoutMs() {
|
|
132
|
+
const configured = Number(firstNonEmpty(process.env.SIRIUS_MCP_HTTP_TIMEOUT_MS, process.env.SIRIUS_API_TIMEOUT_MS) ??
|
|
133
|
+
String(DEFAULT_MCP_HTTP_TIMEOUT_MS));
|
|
134
|
+
if (!Number.isFinite(configured) || configured < 1) {
|
|
135
|
+
return DEFAULT_MCP_HTTP_TIMEOUT_MS;
|
|
136
|
+
}
|
|
137
|
+
return Math.min(Math.floor(configured), MAX_MCP_HTTP_TIMEOUT_MS);
|
|
138
|
+
}
|
|
139
|
+
function trimSlash(value) {
|
|
140
|
+
return value.replace(/\/+$/, "");
|
|
141
|
+
}
|
package/dist/impact.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { enrichGraphNodes, enrichSymbols } from "./enrich.js";
|
|
2
|
+
import { agentPost, indexPost } from "./http.js";
|
|
3
|
+
import { hasStaleSnippet } from "./search.js";
|
|
4
|
+
export async function impactAnalysisWithOptionalStaleSync(options, dependencies = {}) {
|
|
5
|
+
const load = dependencies.load ??
|
|
6
|
+
(() => indexPost("/v1/analysis/impact", {
|
|
7
|
+
repo_id: options.repoId,
|
|
8
|
+
symbol: options.symbol,
|
|
9
|
+
depth: options.depth
|
|
10
|
+
}));
|
|
11
|
+
const sync = dependencies.sync ?? (() => agentPost(`/repos/${encodeURIComponent(options.repoId)}/sync`, { full: false }));
|
|
12
|
+
const enrich = (response) => enrichImpactResponse(response, options, dependencies);
|
|
13
|
+
const initial = await load();
|
|
14
|
+
const initialEnriched = await enrich(initial);
|
|
15
|
+
if (!options.includeCode || !options.autoSyncStale || !impactHasStaleSnippet(initialEnriched)) {
|
|
16
|
+
return initialEnriched;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const syncResult = await sync();
|
|
20
|
+
const refreshed = await load();
|
|
21
|
+
return {
|
|
22
|
+
...(await enrich(refreshed)),
|
|
23
|
+
sync_result: syncResult
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return {
|
|
28
|
+
...initialEnriched,
|
|
29
|
+
sync_error: error instanceof Error ? error.message : String(error)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function enrichImpactResponse(response, options, dependencies) {
|
|
34
|
+
const enrichSymbol = dependencies.enrichSymbol ?? defaultEnrichSymbol;
|
|
35
|
+
const enrichNodes = dependencies.enrichNodes ?? enrichGraphNodes;
|
|
36
|
+
const callerNodes = [...response.direct_callers, ...response.indirect_callers];
|
|
37
|
+
const [symbol, enrichedCallers] = await Promise.all([
|
|
38
|
+
enrichSymbol(options.repoId, response.symbol, options.includeCode),
|
|
39
|
+
enrichNodes(options.repoId, callerNodes, options.includeCode, options.maxCodeNodes)
|
|
40
|
+
]);
|
|
41
|
+
const directCount = response.direct_callers.length;
|
|
42
|
+
return {
|
|
43
|
+
...response,
|
|
44
|
+
symbol,
|
|
45
|
+
direct_callers: enrichedCallers.slice(0, directCount),
|
|
46
|
+
indirect_callers: enrichedCallers.slice(directCount),
|
|
47
|
+
code_enrichment: impactCodeEnrichmentSummary(response, options)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function defaultEnrichSymbol(repoId, symbol, includeCode) {
|
|
51
|
+
if (!symbol) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const [enriched] = await enrichSymbols(repoId, [symbol], includeCode);
|
|
55
|
+
return enriched;
|
|
56
|
+
}
|
|
57
|
+
function impactHasStaleSnippet(response) {
|
|
58
|
+
const snippetAware = [
|
|
59
|
+
...(response.symbol ? [response.symbol] : []),
|
|
60
|
+
...response.direct_callers,
|
|
61
|
+
...response.indirect_callers
|
|
62
|
+
];
|
|
63
|
+
return hasStaleSnippet(snippetAware);
|
|
64
|
+
}
|
|
65
|
+
function impactCodeEnrichmentSummary(response, options) {
|
|
66
|
+
const maxCodeNodes = options.includeCode ? options.maxCodeNodes : 0;
|
|
67
|
+
const callerCount = response.direct_callers.length + response.indirect_callers.length;
|
|
68
|
+
const selectedCallers = Math.min(callerCount, maxCodeNodes);
|
|
69
|
+
const selectedRoot = response.symbol && options.includeCode ? 1 : 0;
|
|
70
|
+
return {
|
|
71
|
+
enabled: options.includeCode,
|
|
72
|
+
max_code_nodes: maxCodeNodes,
|
|
73
|
+
enriched_nodes: selectedRoot + selectedCallers,
|
|
74
|
+
omitted_nodes: Math.max(0, callerCount - selectedCallers)
|
|
75
|
+
};
|
|
76
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { enrichGraphNodes, enrichSearchResults, enrichSymbols } from "./enrich.js";
|
|
6
|
+
import { agentGet, agentPost, indexDelete, indexGet, indexPost } from "./http.js";
|
|
7
|
+
import { impactAnalysisWithOptionalStaleSync } from "./impact.js";
|
|
8
|
+
import { hasStaleSnippet, searchWithOptionalStaleSync } from "./search.js";
|
|
9
|
+
import { localFilePathSchema, publicRepoIdSchema } from "./validation.js";
|
|
10
|
+
// General rule: package-installed CLIs need a no-session self-check path for install validation.
|
|
11
|
+
const MCP_PROXY_VERSION = "0.1.0";
|
|
12
|
+
// General rule: expanded call graphs can include many nodes, while source snippets dominate MCP payload size.
|
|
13
|
+
const DEFAULT_CALL_CHAIN_CODE_NODE_LIMIT = 50;
|
|
14
|
+
// General rule: graph snippet enrichment should stay bounded by the largest accepted call-chain graph window.
|
|
15
|
+
const MAX_CALL_CHAIN_CODE_NODE_LIMIT = 200;
|
|
16
|
+
// General rule: expanded call graphs should preserve a readable edge window without sending dense graph noise by default.
|
|
17
|
+
const DEFAULT_CALL_CHAIN_EDGE_LIMIT = 300;
|
|
18
|
+
// General rule: callers may inspect dense graph neighborhoods, but edge output should remain explicitly bounded.
|
|
19
|
+
const MAX_CALL_CHAIN_EDGE_LIMIT = 2000;
|
|
20
|
+
if (printCliMetadata(process.argv.slice(2))) {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const server = new McpServer({
|
|
24
|
+
name: "sirius-mcp",
|
|
25
|
+
version: MCP_PROXY_VERSION
|
|
26
|
+
});
|
|
27
|
+
server.registerTool("semantic_code_search", {
|
|
28
|
+
title: "Semantic Code Search",
|
|
29
|
+
description: "Find relevant code by meaning using the Sirius index, optionally returning local snippets.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
repo_id: publicRepoIdSchema,
|
|
32
|
+
query: z.string().min(1),
|
|
33
|
+
top_k: z.number().int().min(1).max(50).default(10),
|
|
34
|
+
include_code: z.boolean().default(true),
|
|
35
|
+
auto_sync_stale: z.boolean().default(true)
|
|
36
|
+
}
|
|
37
|
+
}, async ({ repo_id, query, top_k, include_code, auto_sync_stale }) => {
|
|
38
|
+
return asJson(await searchWithOptionalStaleSync({
|
|
39
|
+
includeCode: include_code,
|
|
40
|
+
autoSyncStale: auto_sync_stale,
|
|
41
|
+
search: () => indexPost("/v1/search/semantic", {
|
|
42
|
+
repo_id,
|
|
43
|
+
query,
|
|
44
|
+
top_k,
|
|
45
|
+
include_symbols: true
|
|
46
|
+
}),
|
|
47
|
+
enrich: (results, includeCode) => enrichSearchResults(repo_id, results, includeCode),
|
|
48
|
+
sync: () => agentPost(`/repos/${encodeURIComponent(repo_id)}/sync`, { full: false })
|
|
49
|
+
}));
|
|
50
|
+
});
|
|
51
|
+
server.registerTool("hybrid_search", {
|
|
52
|
+
title: "Hybrid Search",
|
|
53
|
+
description: "Combine semantic, symbol, and file-path relevance using the Sirius index, optionally returning local snippets.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
repo_id: publicRepoIdSchema,
|
|
56
|
+
query: z.string().min(1),
|
|
57
|
+
top_k: z.number().int().min(1).max(50).default(10),
|
|
58
|
+
include_code: z.boolean().default(true),
|
|
59
|
+
auto_sync_stale: z.boolean().default(true)
|
|
60
|
+
}
|
|
61
|
+
}, async ({ repo_id, query, top_k, include_code, auto_sync_stale }) => {
|
|
62
|
+
return asJson(await searchWithOptionalStaleSync({
|
|
63
|
+
includeCode: include_code,
|
|
64
|
+
autoSyncStale: auto_sync_stale,
|
|
65
|
+
search: () => indexPost("/v1/search/hybrid", {
|
|
66
|
+
repo_id,
|
|
67
|
+
query,
|
|
68
|
+
top_k,
|
|
69
|
+
include_graph_context: true
|
|
70
|
+
}),
|
|
71
|
+
enrich: (results, includeCode) => enrichSearchResults(repo_id, results, includeCode),
|
|
72
|
+
sync: () => agentPost(`/repos/${encodeURIComponent(repo_id)}/sync`, { full: false })
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
75
|
+
server.registerTool("path_search", {
|
|
76
|
+
title: "Path Search",
|
|
77
|
+
description: "Find indexed files by path relevance.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
repo_id: publicRepoIdSchema,
|
|
80
|
+
query: z.string().min(1),
|
|
81
|
+
top_k: z.number().int().min(1).max(100).default(10)
|
|
82
|
+
}
|
|
83
|
+
}, async ({ repo_id, query, top_k }) => {
|
|
84
|
+
const response = await indexPost("/v1/search/path", { repo_id, query, top_k });
|
|
85
|
+
return asJson(response);
|
|
86
|
+
});
|
|
87
|
+
server.registerTool("symbol_search", {
|
|
88
|
+
title: "Symbol Search",
|
|
89
|
+
description: "Find functions, classes, types, methods, and modules by exact or fuzzy name.",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
repo_id: publicRepoIdSchema,
|
|
92
|
+
symbol: z.string().min(1),
|
|
93
|
+
kind: z.string().optional(),
|
|
94
|
+
top_k: z.number().int().min(1).max(50).default(10),
|
|
95
|
+
include_code: z.boolean().default(true),
|
|
96
|
+
auto_sync_stale: z.boolean().default(true)
|
|
97
|
+
}
|
|
98
|
+
}, async ({ repo_id, symbol, kind, top_k, include_code, auto_sync_stale }) => {
|
|
99
|
+
return asJson(await searchWithOptionalStaleSync({
|
|
100
|
+
includeCode: include_code,
|
|
101
|
+
autoSyncStale: auto_sync_stale,
|
|
102
|
+
search: () => indexPost("/v1/search/symbol", {
|
|
103
|
+
repo_id,
|
|
104
|
+
symbol,
|
|
105
|
+
kind,
|
|
106
|
+
top_k
|
|
107
|
+
}),
|
|
108
|
+
enrich: (results, includeCode) => enrichSymbols(repo_id, results, includeCode),
|
|
109
|
+
sync: () => agentPost(`/repos/${encodeURIComponent(repo_id)}/sync`, { full: false })
|
|
110
|
+
}));
|
|
111
|
+
});
|
|
112
|
+
server.registerTool("get_file_range", {
|
|
113
|
+
title: "Get File Range",
|
|
114
|
+
description: "Fetch exact source lines from the local repo agent.",
|
|
115
|
+
inputSchema: {
|
|
116
|
+
repo_id: publicRepoIdSchema,
|
|
117
|
+
file_path: localFilePathSchema,
|
|
118
|
+
start_line: z.number().int().min(1),
|
|
119
|
+
end_line: z.number().int().min(1)
|
|
120
|
+
}
|
|
121
|
+
}, async ({ repo_id, file_path, start_line, end_line }) => {
|
|
122
|
+
const params = new URLSearchParams({
|
|
123
|
+
file_path,
|
|
124
|
+
start_line: String(start_line),
|
|
125
|
+
end_line: String(end_line)
|
|
126
|
+
});
|
|
127
|
+
const response = await agentGet(`/repos/${encodeURIComponent(repo_id)}/file-range?${params.toString()}`);
|
|
128
|
+
return asJson(response);
|
|
129
|
+
});
|
|
130
|
+
server.registerTool("get_call_chain", {
|
|
131
|
+
title: "Get Call Chain",
|
|
132
|
+
description: "Traverse caller/callee relationships for a symbol.",
|
|
133
|
+
inputSchema: {
|
|
134
|
+
repo_id: publicRepoIdSchema,
|
|
135
|
+
symbol_id: z.string().optional(),
|
|
136
|
+
symbol: z.string().optional(),
|
|
137
|
+
direction: z.enum(["callers", "callees", "both"]).default("callers"),
|
|
138
|
+
depth: z.number().int().min(1).max(8).default(3),
|
|
139
|
+
max_nodes: z.number().int().min(1).max(200).default(50),
|
|
140
|
+
max_edges: z.number().int().min(0).max(MAX_CALL_CHAIN_EDGE_LIMIT).default(DEFAULT_CALL_CHAIN_EDGE_LIMIT),
|
|
141
|
+
max_unresolved: z.number().int().min(0).max(1000).default(200),
|
|
142
|
+
max_code_nodes: z.number().int().min(0).max(MAX_CALL_CHAIN_CODE_NODE_LIMIT).default(DEFAULT_CALL_CHAIN_CODE_NODE_LIMIT),
|
|
143
|
+
include_code: z.boolean().default(true),
|
|
144
|
+
auto_sync_stale: z.boolean().default(true)
|
|
145
|
+
}
|
|
146
|
+
}, async ({ repo_id, symbol_id, symbol, direction, depth, max_nodes, max_edges, max_unresolved, max_code_nodes, include_code, auto_sync_stale }) => {
|
|
147
|
+
return asJson(await callChainWithOptionalStaleSync({
|
|
148
|
+
repoId: repo_id,
|
|
149
|
+
symbolId: symbol_id,
|
|
150
|
+
symbol,
|
|
151
|
+
direction,
|
|
152
|
+
depth,
|
|
153
|
+
maxNodes: max_nodes,
|
|
154
|
+
maxEdges: max_edges,
|
|
155
|
+
maxUnresolved: max_unresolved,
|
|
156
|
+
maxCodeNodes: max_code_nodes,
|
|
157
|
+
includeCode: include_code,
|
|
158
|
+
autoSyncStale: auto_sync_stale
|
|
159
|
+
}));
|
|
160
|
+
});
|
|
161
|
+
server.registerTool("impact_analysis", {
|
|
162
|
+
title: "Impact Analysis",
|
|
163
|
+
description: "Estimate blast radius for a symbol by traversing callers and related files.",
|
|
164
|
+
inputSchema: {
|
|
165
|
+
repo_id: publicRepoIdSchema,
|
|
166
|
+
symbol: z.string().min(1),
|
|
167
|
+
depth: z.number().int().min(1).max(8).default(3),
|
|
168
|
+
max_code_nodes: z.number().int().min(0).max(MAX_CALL_CHAIN_CODE_NODE_LIMIT).default(DEFAULT_CALL_CHAIN_CODE_NODE_LIMIT),
|
|
169
|
+
include_code: z.boolean().default(true),
|
|
170
|
+
auto_sync_stale: z.boolean().default(true)
|
|
171
|
+
}
|
|
172
|
+
}, async ({ repo_id, symbol, depth, max_code_nodes, include_code, auto_sync_stale }) => {
|
|
173
|
+
return asJson(await impactAnalysisWithOptionalStaleSync({
|
|
174
|
+
repoId: repo_id,
|
|
175
|
+
symbol,
|
|
176
|
+
depth,
|
|
177
|
+
maxCodeNodes: max_code_nodes,
|
|
178
|
+
includeCode: include_code,
|
|
179
|
+
autoSyncStale: auto_sync_stale
|
|
180
|
+
}));
|
|
181
|
+
});
|
|
182
|
+
server.registerTool("ripgrep_search", {
|
|
183
|
+
title: "Ripgrep Search",
|
|
184
|
+
description: "Run exact text search locally inside a registered repo.",
|
|
185
|
+
inputSchema: {
|
|
186
|
+
repo_id: publicRepoIdSchema,
|
|
187
|
+
pattern: z.string().min(1),
|
|
188
|
+
glob: z.string().optional(),
|
|
189
|
+
max_results: z.number().int().min(1).max(200).default(50)
|
|
190
|
+
}
|
|
191
|
+
}, async ({ repo_id, pattern, glob, max_results }) => {
|
|
192
|
+
const response = await agentPost(`/repos/${encodeURIComponent(repo_id)}/ripgrep`, {
|
|
193
|
+
pattern,
|
|
194
|
+
glob,
|
|
195
|
+
max_results
|
|
196
|
+
});
|
|
197
|
+
return asJson(response);
|
|
198
|
+
});
|
|
199
|
+
server.registerTool("sync_repo", {
|
|
200
|
+
title: "Sync Repo",
|
|
201
|
+
description: "Trigger a manual incremental or full sync through the local repo agent.",
|
|
202
|
+
inputSchema: {
|
|
203
|
+
repo_id: publicRepoIdSchema,
|
|
204
|
+
full: z.boolean().default(false)
|
|
205
|
+
}
|
|
206
|
+
}, async ({ repo_id, full }) => {
|
|
207
|
+
const response = await agentPost(`/repos/${encodeURIComponent(repo_id)}/sync`, { full });
|
|
208
|
+
return asJson(response);
|
|
209
|
+
});
|
|
210
|
+
server.registerTool("sync_status", {
|
|
211
|
+
title: "Sync Status",
|
|
212
|
+
description: "Return local repo-agent status and server index status.",
|
|
213
|
+
inputSchema: {
|
|
214
|
+
repo_id: publicRepoIdSchema
|
|
215
|
+
}
|
|
216
|
+
}, async ({ repo_id }) => {
|
|
217
|
+
const [serverStatus, agentStatus] = await Promise.allSettled([
|
|
218
|
+
indexGet(`/v1/repos/${encodeURIComponent(repo_id)}/status`),
|
|
219
|
+
agentGet(`/repos/${encodeURIComponent(repo_id)}/status`)
|
|
220
|
+
]);
|
|
221
|
+
return asJson({
|
|
222
|
+
server: settledValue(serverStatus),
|
|
223
|
+
agent: settledValue(agentStatus)
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
server.registerTool("delete_repo_index", {
|
|
227
|
+
title: "Delete Repo Index",
|
|
228
|
+
description: "Purge derived index data for a repo from the index server.",
|
|
229
|
+
inputSchema: {
|
|
230
|
+
repo_id: publicRepoIdSchema
|
|
231
|
+
}
|
|
232
|
+
}, async ({ repo_id }) => {
|
|
233
|
+
const response = await indexDelete(`/v1/repos/${encodeURIComponent(repo_id)}`);
|
|
234
|
+
return asJson(response);
|
|
235
|
+
});
|
|
236
|
+
const transport = new StdioServerTransport();
|
|
237
|
+
await server.connect(transport);
|
|
238
|
+
function asJson(value) {
|
|
239
|
+
return {
|
|
240
|
+
content: [
|
|
241
|
+
{
|
|
242
|
+
type: "text",
|
|
243
|
+
text: JSON.stringify(value, null, 2)
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function settledValue(result) {
|
|
249
|
+
if (result.status === "fulfilled") {
|
|
250
|
+
return result.value;
|
|
251
|
+
}
|
|
252
|
+
return { error: result.reason instanceof Error ? result.reason.message : String(result.reason) };
|
|
253
|
+
}
|
|
254
|
+
async function callChainWithOptionalStaleSync(options) {
|
|
255
|
+
const load = () => indexPost("/v1/graph/call-chain", {
|
|
256
|
+
repo_id: options.repoId,
|
|
257
|
+
symbol_id: options.symbolId,
|
|
258
|
+
symbol: options.symbol,
|
|
259
|
+
direction: options.direction,
|
|
260
|
+
depth: options.depth,
|
|
261
|
+
max_nodes: options.maxNodes,
|
|
262
|
+
max_edges: options.maxEdges,
|
|
263
|
+
max_unresolved: options.maxUnresolved
|
|
264
|
+
});
|
|
265
|
+
const initial = await load();
|
|
266
|
+
const initialNodes = await enrichGraphNodes(options.repoId, initial.nodes, options.includeCode, options.maxCodeNodes);
|
|
267
|
+
if (!options.includeCode || !options.autoSyncStale || !hasStaleSnippet(initialNodes)) {
|
|
268
|
+
return { ...initial, nodes: initialNodes, code_enrichment: codeEnrichmentSummary(initial.nodes.length, options) };
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
const syncResult = await agentPost(`/repos/${encodeURIComponent(options.repoId)}/sync`, { full: false });
|
|
272
|
+
const refreshed = await load();
|
|
273
|
+
return {
|
|
274
|
+
...refreshed,
|
|
275
|
+
nodes: await enrichGraphNodes(options.repoId, refreshed.nodes, options.includeCode, options.maxCodeNodes),
|
|
276
|
+
code_enrichment: codeEnrichmentSummary(refreshed.nodes.length, options),
|
|
277
|
+
sync_result: syncResult
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
return {
|
|
282
|
+
...initial,
|
|
283
|
+
nodes: initialNodes,
|
|
284
|
+
code_enrichment: codeEnrichmentSummary(initial.nodes.length, options),
|
|
285
|
+
sync_error: error instanceof Error ? error.message : String(error)
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function codeEnrichmentSummary(nodeCount, options) {
|
|
290
|
+
const maxCodeNodes = options.includeCode ? options.maxCodeNodes : 0;
|
|
291
|
+
const enrichedNodes = Math.min(nodeCount, maxCodeNodes);
|
|
292
|
+
return {
|
|
293
|
+
enabled: options.includeCode,
|
|
294
|
+
max_code_nodes: maxCodeNodes,
|
|
295
|
+
enriched_nodes: enrichedNodes,
|
|
296
|
+
omitted_nodes: Math.max(0, nodeCount - enrichedNodes)
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function printCliMetadata(args) {
|
|
300
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
301
|
+
console.log(MCP_PROXY_VERSION);
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
305
|
+
console.log([
|
|
306
|
+
"Usage: sirius-mcp-proxy [--version] [--help]",
|
|
307
|
+
"",
|
|
308
|
+
"Runs the Sirius MCP stdio proxy.",
|
|
309
|
+
"Configure SIRIUS_INDEX_URL, SIRIUS_AGENT_URL, and SIRIUS_API_TOKEN_FILE as needed."
|
|
310
|
+
].join("\n"));
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
return false;
|
|
314
|
+
}
|
package/dist/search.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function searchWithOptionalStaleSync(options) {
|
|
2
|
+
const initial = await options.search();
|
|
3
|
+
const initialResults = await options.enrich(initial.results, options.includeCode);
|
|
4
|
+
if (!options.includeCode || !options.autoSyncStale || !hasStaleSnippet(initialResults)) {
|
|
5
|
+
return { results: initialResults };
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
const syncResult = await options.sync();
|
|
9
|
+
const refreshed = await options.search();
|
|
10
|
+
return {
|
|
11
|
+
results: await options.enrich(refreshed.results, options.includeCode),
|
|
12
|
+
sync_result: syncResult
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
return {
|
|
17
|
+
results: initialResults,
|
|
18
|
+
sync_error: error instanceof Error ? error.message : String(error)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function hasStaleSnippet(results) {
|
|
23
|
+
return results.some((result) => result.snippet?.stale === true);
|
|
24
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// General rule: public repo ids must not collide with scoped storage keys or path routing.
|
|
3
|
+
const PUBLIC_REPO_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
4
|
+
// General rule: local file reads must use normalized repo-relative slash paths.
|
|
5
|
+
const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:/;
|
|
6
|
+
export const PUBLIC_REPO_ID_RULE = "use 1-128 ASCII letters, digits, dots, underscores, or hyphens, starting with a letter or digit";
|
|
7
|
+
export const LOCAL_FILE_PATH_RULE = "file_path must be a relative path inside the repo";
|
|
8
|
+
export const publicRepoIdSchema = z.string().regex(PUBLIC_REPO_ID_PATTERN, PUBLIC_REPO_ID_RULE);
|
|
9
|
+
export const localFilePathSchema = z.string().refine(isSafeLocalFilePath, LOCAL_FILE_PATH_RULE);
|
|
10
|
+
export function isSafeLocalFilePath(filePath) {
|
|
11
|
+
if (!filePath ||
|
|
12
|
+
filePath.includes("\0") ||
|
|
13
|
+
filePath.includes("\\") ||
|
|
14
|
+
filePath.startsWith("/") ||
|
|
15
|
+
WINDOWS_DRIVE_PATH_PATTERN.test(filePath)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return filePath.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@harivatsa/sirius-mcp-proxy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP proxy that joins Sirius index metadata with local source snippets.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"sirius",
|
|
9
|
+
"mcp",
|
|
10
|
+
"code-search",
|
|
11
|
+
"claude"
|
|
12
|
+
],
|
|
13
|
+
"bin": {
|
|
14
|
+
"sirius-mcp-proxy": "dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"dev": "tsx src/index.ts",
|
|
22
|
+
"prepack": "npm run build",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
+
"test": "tsc -p tsconfig.test.json --noEmit && node --import tsx --test \"test/*.test.ts\""
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
28
|
+
"zod": "^3.25.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^24.0.10",
|
|
32
|
+
"tsx": "^4.20.3",
|
|
33
|
+
"typescript": "^5.8.3"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
}
|
|
41
|
+
}
|