domain-rag-mcp-server 3.1.1 → 3.3.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/index.mjs +560 -2
- package/package.json +47 -46
package/dist/index.mjs
CHANGED
|
@@ -210,6 +210,12 @@ function buildConfig() {
|
|
|
210
210
|
codeSearchApi: {
|
|
211
211
|
enabled: getEnvBool("CODE_SEARCH_API_ENABLED", true),
|
|
212
212
|
url: getEnv("CODE_SEARCH_API_URL", "http://10.3.1.94:8085")
|
|
213
|
+
},
|
|
214
|
+
datadog: {
|
|
215
|
+
enabled: getEnvBool("DATADOG_ENABLED", false),
|
|
216
|
+
apiKey: getEnv("DATADOG_API_KEY", ""),
|
|
217
|
+
appKey: getEnv("DATADOG_APP_KEY", ""),
|
|
218
|
+
site: getEnv("DATADOG_SITE", "datadoghq.eu")
|
|
213
219
|
}
|
|
214
220
|
};
|
|
215
221
|
}
|
|
@@ -737,6 +743,35 @@ async function getStats() {
|
|
|
737
743
|
};
|
|
738
744
|
}
|
|
739
745
|
|
|
746
|
+
// ../shared/src/graph-db.ts
|
|
747
|
+
async function searchGraphEntities(query_text, entityTypes, limit = 10) {
|
|
748
|
+
const params = [`%${query_text.toLowerCase()}%`];
|
|
749
|
+
let typeFilter = "";
|
|
750
|
+
if (entityTypes && entityTypes.length > 0) {
|
|
751
|
+
params.push(entityTypes);
|
|
752
|
+
typeFilter = `AND entity_type = ANY($${params.length}::text[])`;
|
|
753
|
+
}
|
|
754
|
+
params.push(limit);
|
|
755
|
+
return queryRows(
|
|
756
|
+
`SELECT * FROM graph_entities
|
|
757
|
+
WHERE (LOWER(name) LIKE $1 OR EXISTS (
|
|
758
|
+
SELECT 1 FROM unnest(aliases) a WHERE LOWER(a) LIKE $1
|
|
759
|
+
))
|
|
760
|
+
${typeFilter}
|
|
761
|
+
ORDER BY degree DESC
|
|
762
|
+
LIMIT $${params.length}`,
|
|
763
|
+
params
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
async function getGraphNeighbors(startEntityIds, maxHops = 2) {
|
|
767
|
+
if (startEntityIds.length === 0)
|
|
768
|
+
return [];
|
|
769
|
+
return queryRows(
|
|
770
|
+
`SELECT * FROM get_graph_neighbors($1::text[], $2)`,
|
|
771
|
+
[startEntityIds, maxHops]
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
740
775
|
// src/index.ts
|
|
741
776
|
var SOURCE_WEIGHTS = getSourceWeightsMap();
|
|
742
777
|
var qdrant = new QdrantClient({
|
|
@@ -988,6 +1023,25 @@ var tools = [
|
|
|
988
1023
|
properties: {}
|
|
989
1024
|
}
|
|
990
1025
|
},
|
|
1026
|
+
{
|
|
1027
|
+
name: "raw_sql",
|
|
1028
|
+
description: "Execute a raw SQL SELECT query on PostgreSQL database. ONLY SELECT queries are allowed. Use for complex queries that other tools cannot handle. Tables: persons, jira_projects, jira_issues, confluence_spaces, confluence_pages, git_commits, jira_issue_commits.",
|
|
1029
|
+
inputSchema: {
|
|
1030
|
+
type: "object",
|
|
1031
|
+
properties: {
|
|
1032
|
+
sql: {
|
|
1033
|
+
type: "string",
|
|
1034
|
+
description: "SQL SELECT query to execute. Example: SELECT issue_key, summary FROM jira_issues WHERE status = 'Done' LIMIT 10"
|
|
1035
|
+
},
|
|
1036
|
+
params: {
|
|
1037
|
+
type: "array",
|
|
1038
|
+
items: { type: "string" },
|
|
1039
|
+
description: "Query parameters for parameterized queries (optional). Use $1, $2, etc. in SQL."
|
|
1040
|
+
}
|
|
1041
|
+
},
|
|
1042
|
+
required: ["sql"]
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
991
1045
|
// ============================================
|
|
992
1046
|
// Vector Search Tools (Qdrant)
|
|
993
1047
|
// ============================================
|
|
@@ -1190,6 +1244,52 @@ var tools = [
|
|
|
1190
1244
|
}
|
|
1191
1245
|
},
|
|
1192
1246
|
// ============================================
|
|
1247
|
+
// GraphRAG Tools — graph-enhanced search
|
|
1248
|
+
// ============================================
|
|
1249
|
+
{
|
|
1250
|
+
name: "graph_search",
|
|
1251
|
+
description: "Graph-enhanced hybrid search. Combines Qdrant vector search with knowledge graph traversal to return connected results with relationship context. Best for complex queries that span multiple sources (Jira + Confluence + Git + People). Use when you need to understand relationships, not just find similar text.",
|
|
1252
|
+
inputSchema: {
|
|
1253
|
+
type: "object",
|
|
1254
|
+
properties: {
|
|
1255
|
+
query: { type: "string", description: "What are you looking for?" },
|
|
1256
|
+
limit: { type: "number", description: "Max results (default: 15)", default: 15 },
|
|
1257
|
+
source_types: {
|
|
1258
|
+
type: "array",
|
|
1259
|
+
items: { type: "string", enum: ["jira", "confluence", "git_commit", "code", "decision", "domain_term"] },
|
|
1260
|
+
description: "Filter by source types (optional)"
|
|
1261
|
+
},
|
|
1262
|
+
graph_hops: { type: "number", description: "Graph traversal depth: 1 or 2 (default: 1)", default: 1 }
|
|
1263
|
+
},
|
|
1264
|
+
required: ["query"]
|
|
1265
|
+
}
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
name: "entity_search",
|
|
1269
|
+
description: 'Find everything connected to a specific entity (person, service, Jira project, component). Uses knowledge graph to collect all related Jira issues, Confluence pages, Git commits, and people in one query. Use for "tell me everything about X" queries.',
|
|
1270
|
+
inputSchema: {
|
|
1271
|
+
type: "object",
|
|
1272
|
+
properties: {
|
|
1273
|
+
entity: { type: "string", description: 'Entity name or ID (e.g. "PaymentGateway", "Ivan Petrov", "PROJ-123", "AUTH")' },
|
|
1274
|
+
max_hops: { type: "number", description: "Relationship hops to traverse (default: 2)", default: 2 },
|
|
1275
|
+
limit: { type: "number", description: "Max results (default: 20)", default: 20 }
|
|
1276
|
+
},
|
|
1277
|
+
required: ["entity"]
|
|
1278
|
+
}
|
|
1279
|
+
},
|
|
1280
|
+
{
|
|
1281
|
+
name: "impact_analysis",
|
|
1282
|
+
description: "Analyze the impact of changing a service, module, or component. Traverses the knowledge graph to find all dependent services, teams, active issues, and documentation that would be affected. Use before making significant changes.",
|
|
1283
|
+
inputSchema: {
|
|
1284
|
+
type: "object",
|
|
1285
|
+
properties: {
|
|
1286
|
+
target: { type: "string", description: 'Service, module, or component name (e.g. "auth-service", "PaymentProcessor")' },
|
|
1287
|
+
max_depth: { type: "number", description: "Traversal depth (default: 3)", default: 3 }
|
|
1288
|
+
},
|
|
1289
|
+
required: ["target"]
|
|
1290
|
+
}
|
|
1291
|
+
},
|
|
1292
|
+
// ============================================
|
|
1193
1293
|
// Server Code Search Tools (LOW PRIORITY - use last)
|
|
1194
1294
|
// Direct grep on server filesystem repositories
|
|
1195
1295
|
// WARNING: Returns large responses that consume context window.
|
|
@@ -1238,7 +1338,42 @@ var tools = [
|
|
|
1238
1338
|
type: "object",
|
|
1239
1339
|
properties: {}
|
|
1240
1340
|
}
|
|
1241
|
-
}
|
|
1341
|
+
},
|
|
1342
|
+
// ============================================
|
|
1343
|
+
// Datadog Log Search Tool
|
|
1344
|
+
// ============================================
|
|
1345
|
+
...config.datadog.enabled ? [{
|
|
1346
|
+
name: "search_datadog_logs",
|
|
1347
|
+
description: 'Search application logs in Datadog. Use this to investigate errors, debug issues, check service health, or find specific log entries. Supports Datadog query syntax (e.g. "service:api status:error", "@http.status_code:500").',
|
|
1348
|
+
inputSchema: {
|
|
1349
|
+
type: "object",
|
|
1350
|
+
properties: {
|
|
1351
|
+
query: {
|
|
1352
|
+
type: "string",
|
|
1353
|
+
description: 'Datadog log search query. Examples: "error", "service:my-api status:error", "@http.url:/api/users", "timeout OR connection refused"'
|
|
1354
|
+
},
|
|
1355
|
+
service: {
|
|
1356
|
+
type: "string",
|
|
1357
|
+
description: "Filter by service name (optional, appended to query as service:<value>)"
|
|
1358
|
+
},
|
|
1359
|
+
env: {
|
|
1360
|
+
type: "string",
|
|
1361
|
+
description: 'Filter by environment (optional, e.g. "production", "staging")'
|
|
1362
|
+
},
|
|
1363
|
+
time_range: {
|
|
1364
|
+
type: "string",
|
|
1365
|
+
description: 'Time range to search (default: "1h"). Supported: "15m", "1h", "6h", "24h", "3d", "7d"',
|
|
1366
|
+
default: "1h"
|
|
1367
|
+
},
|
|
1368
|
+
limit: {
|
|
1369
|
+
type: "number",
|
|
1370
|
+
description: "Maximum log entries to return (default: 30, max: 100)",
|
|
1371
|
+
default: 30
|
|
1372
|
+
}
|
|
1373
|
+
},
|
|
1374
|
+
required: ["query"]
|
|
1375
|
+
}
|
|
1376
|
+
}] : []
|
|
1242
1377
|
];
|
|
1243
1378
|
async function handleSearchServerCode(args) {
|
|
1244
1379
|
if (!config.codeSearchApi.enabled) {
|
|
@@ -1569,6 +1704,59 @@ async function handleGetDbStats() {
|
|
|
1569
1704
|
}
|
|
1570
1705
|
return response;
|
|
1571
1706
|
}
|
|
1707
|
+
async function handleRawSql(args) {
|
|
1708
|
+
if (!pgAvailable) {
|
|
1709
|
+
return "PostgreSQL not available.";
|
|
1710
|
+
}
|
|
1711
|
+
const normalizedSql = args.sql.trim().toLowerCase();
|
|
1712
|
+
if (!normalizedSql.startsWith("select")) {
|
|
1713
|
+
return "Error: Only SELECT queries are allowed. Use SELECT to query data.";
|
|
1714
|
+
}
|
|
1715
|
+
const dangerousKeywords = ["insert", "update", "delete", "drop", "truncate", "alter", "create", "grant", "revoke"];
|
|
1716
|
+
for (const keyword of dangerousKeywords) {
|
|
1717
|
+
if (normalizedSql.includes(keyword)) {
|
|
1718
|
+
return `Error: Query contains forbidden keyword "${keyword}". Only SELECT queries are allowed.`;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
try {
|
|
1722
|
+
const result = await query(args.sql, args.params);
|
|
1723
|
+
if (result.rows.length === 0) {
|
|
1724
|
+
return "Query returned no results.";
|
|
1725
|
+
}
|
|
1726
|
+
const columns = Object.keys(result.rows[0]);
|
|
1727
|
+
let response = `## Query Results
|
|
1728
|
+
|
|
1729
|
+
`;
|
|
1730
|
+
response += `Rows: ${result.rows.length}
|
|
1731
|
+
|
|
1732
|
+
`;
|
|
1733
|
+
response += "| " + columns.join(" | ") + " |\n";
|
|
1734
|
+
response += "|" + columns.map(() => "---").join("|") + "|\n";
|
|
1735
|
+
const maxRows = Math.min(result.rows.length, 100);
|
|
1736
|
+
for (let i = 0; i < maxRows; i++) {
|
|
1737
|
+
const row = result.rows[i];
|
|
1738
|
+
const values = columns.map((col) => {
|
|
1739
|
+
const val = row[col];
|
|
1740
|
+
if (val === null)
|
|
1741
|
+
return "NULL";
|
|
1742
|
+
if (typeof val === "object")
|
|
1743
|
+
return JSON.stringify(val).substring(0, 100);
|
|
1744
|
+
const str = String(val);
|
|
1745
|
+
return str.length > 100 ? str.substring(0, 100) + "..." : str;
|
|
1746
|
+
});
|
|
1747
|
+
response += "| " + values.join(" | ") + " |\n";
|
|
1748
|
+
}
|
|
1749
|
+
if (result.rows.length > 100) {
|
|
1750
|
+
response += `
|
|
1751
|
+
*Showing first 100 of ${result.rows.length} rows*
|
|
1752
|
+
`;
|
|
1753
|
+
}
|
|
1754
|
+
return response;
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1757
|
+
return `SQL Error: ${message}`;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1572
1760
|
async function handleSearchAsAnalyst(args) {
|
|
1573
1761
|
const results = await searchWithWeights({
|
|
1574
1762
|
query: args.query,
|
|
@@ -1741,6 +1929,360 @@ async function handleGetStats() {
|
|
|
1741
1929
|
}
|
|
1742
1930
|
return response;
|
|
1743
1931
|
}
|
|
1932
|
+
async function handleGraphSearch(args) {
|
|
1933
|
+
const { query: query2, limit = 15, source_types, graph_hops = 1 } = args;
|
|
1934
|
+
const embedding = await getEmbedding(query2);
|
|
1935
|
+
const filterMust = [];
|
|
1936
|
+
if (source_types && source_types.length > 0) {
|
|
1937
|
+
filterMust.push({ key: "source_type", match: { any: source_types } });
|
|
1938
|
+
}
|
|
1939
|
+
const [chunkResults, entityResults] = await Promise.all([
|
|
1940
|
+
qdrant.search(config.qdrant.collectionName, {
|
|
1941
|
+
vector: embedding,
|
|
1942
|
+
limit: limit * 3,
|
|
1943
|
+
filter: filterMust.length > 0 ? { must: filterMust } : void 0,
|
|
1944
|
+
with_payload: true,
|
|
1945
|
+
score_threshold: 0.25
|
|
1946
|
+
}),
|
|
1947
|
+
qdrant.search("graph_entities", {
|
|
1948
|
+
vector: embedding,
|
|
1949
|
+
limit: 20,
|
|
1950
|
+
with_payload: true,
|
|
1951
|
+
score_threshold: 0.3
|
|
1952
|
+
}).catch(() => [])
|
|
1953
|
+
// graceful fallback if collection doesn't exist yet
|
|
1954
|
+
]);
|
|
1955
|
+
const topEntityIds = entityResults.slice(0, 10).map((r) => r.payload.entity_id).filter(Boolean);
|
|
1956
|
+
let expandedEntityIds = new Set(topEntityIds);
|
|
1957
|
+
if (topEntityIds.length > 0 && pgAvailable) {
|
|
1958
|
+
try {
|
|
1959
|
+
const neighbors = await getGraphNeighbors(topEntityIds, graph_hops);
|
|
1960
|
+
for (const n of neighbors)
|
|
1961
|
+
expandedEntityIds.add(n.entity_id);
|
|
1962
|
+
} catch {
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
const topEntitySet = new Set(topEntityIds.slice(0, 5));
|
|
1966
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1967
|
+
for (const r of chunkResults) {
|
|
1968
|
+
const payload = r.payload;
|
|
1969
|
+
const sourceType = payload.source_type || "";
|
|
1970
|
+
const sourceId = payload.source_id || "";
|
|
1971
|
+
const chunkEntityIds = payload.entity_ids || [];
|
|
1972
|
+
const weight = SOURCE_WEIGHTS[sourceType] || 1;
|
|
1973
|
+
const connectedCount = chunkEntityIds.filter((e) => topEntitySet.has(e) || expandedEntityIds.has(e)).length;
|
|
1974
|
+
const graphBoost = 1 + 0.3 * Math.min(connectedCount / 3, 1);
|
|
1975
|
+
const finalScore = (r.score || 0) * graphBoost * weight;
|
|
1976
|
+
const key = `${sourceType}:${sourceId}`;
|
|
1977
|
+
const existing = seen.get(key);
|
|
1978
|
+
if (!existing || finalScore > existing.score) {
|
|
1979
|
+
seen.set(key, { score: finalScore, payload });
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
const ranked = [...seen.values()].sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1983
|
+
if (ranked.length === 0) {
|
|
1984
|
+
return `No results found for graph search: "${query2}"`;
|
|
1985
|
+
}
|
|
1986
|
+
let response = `## Graph Search: "${query2}"
|
|
1987
|
+
|
|
1988
|
+
`;
|
|
1989
|
+
response += `Found ${ranked.length} results (vector + graph-enhanced):
|
|
1990
|
+
|
|
1991
|
+
`;
|
|
1992
|
+
if (expandedEntityIds.size > 0) {
|
|
1993
|
+
response += `Graph context: ${expandedEntityIds.size} connected entities found
|
|
1994
|
+
|
|
1995
|
+
`;
|
|
1996
|
+
}
|
|
1997
|
+
for (const { score, payload } of ranked) {
|
|
1998
|
+
const sourceType = payload.source_type || "";
|
|
1999
|
+
const sourceId = payload.source_id || "";
|
|
2000
|
+
const content = payload.content || "";
|
|
2001
|
+
const url = payload.source_url;
|
|
2002
|
+
const communityId = payload.community_id;
|
|
2003
|
+
response += `### [${sourceType.toUpperCase()}] ${sourceId}
|
|
2004
|
+
`;
|
|
2005
|
+
response += `Score: ${(score * 100).toFixed(1)}%`;
|
|
2006
|
+
if (communityId)
|
|
2007
|
+
response += ` | Community: ${communityId}`;
|
|
2008
|
+
response += "\n";
|
|
2009
|
+
if (url)
|
|
2010
|
+
response += `URL: ${url}
|
|
2011
|
+
`;
|
|
2012
|
+
response += `
|
|
2013
|
+
${content.substring(0, 1e3)}${content.length > 1e3 ? "..." : ""}
|
|
2014
|
+
|
|
2015
|
+
---
|
|
2016
|
+
|
|
2017
|
+
`;
|
|
2018
|
+
}
|
|
2019
|
+
return response;
|
|
2020
|
+
}
|
|
2021
|
+
async function handleEntitySearch(args) {
|
|
2022
|
+
const { entity, max_hops = 2, limit = 20 } = args;
|
|
2023
|
+
if (!pgAvailable) {
|
|
2024
|
+
return handleGraphSearch({ query: entity, limit });
|
|
2025
|
+
}
|
|
2026
|
+
let matchedEntities = [];
|
|
2027
|
+
try {
|
|
2028
|
+
matchedEntities = await searchGraphEntities(entity, void 0, 5);
|
|
2029
|
+
} catch {
|
|
2030
|
+
}
|
|
2031
|
+
if (matchedEntities.length === 0) {
|
|
2032
|
+
const embedding2 = await getEmbedding(entity);
|
|
2033
|
+
const qdrantEntities = await qdrant.search("graph_entities", {
|
|
2034
|
+
vector: embedding2,
|
|
2035
|
+
limit: 5,
|
|
2036
|
+
with_payload: true,
|
|
2037
|
+
score_threshold: 0.4
|
|
2038
|
+
}).catch(() => []);
|
|
2039
|
+
matchedEntities = qdrantEntities.map((r) => {
|
|
2040
|
+
const p = r.payload;
|
|
2041
|
+
return {
|
|
2042
|
+
entity_id: p.entity_id,
|
|
2043
|
+
entity_type: p.entity_type,
|
|
2044
|
+
name: p.name
|
|
2045
|
+
};
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
if (matchedEntities.length === 0) {
|
|
2049
|
+
return `Entity "${entity}" not found in knowledge graph. Try graph_search instead.`;
|
|
2050
|
+
}
|
|
2051
|
+
const primaryEntity = matchedEntities[0];
|
|
2052
|
+
let response = `## Entity Search: "${entity}"
|
|
2053
|
+
|
|
2054
|
+
`;
|
|
2055
|
+
response += `**Found entity:** ${primaryEntity.name} (${primaryEntity.entity_type})
|
|
2056
|
+
`;
|
|
2057
|
+
response += `**Entity ID:** ${primaryEntity.entity_id}
|
|
2058
|
+
|
|
2059
|
+
`;
|
|
2060
|
+
let neighbors = [];
|
|
2061
|
+
try {
|
|
2062
|
+
neighbors = await getGraphNeighbors([primaryEntity.entity_id], max_hops);
|
|
2063
|
+
} catch {
|
|
2064
|
+
}
|
|
2065
|
+
if (neighbors.length > 0) {
|
|
2066
|
+
response += `**Connected entities (${neighbors.length}):**
|
|
2067
|
+
|
|
2068
|
+
`;
|
|
2069
|
+
const byType = /* @__PURE__ */ new Map();
|
|
2070
|
+
for (const n of neighbors.slice(0, 50)) {
|
|
2071
|
+
const group = byType.get(n.entity_type) ?? [];
|
|
2072
|
+
group.push(n);
|
|
2073
|
+
byType.set(n.entity_type, group);
|
|
2074
|
+
}
|
|
2075
|
+
for (const [type, nodes] of byType) {
|
|
2076
|
+
response += `**${type}** (${nodes.length}):
|
|
2077
|
+
`;
|
|
2078
|
+
for (const n of nodes.slice(0, 10)) {
|
|
2079
|
+
response += ` - ${n.name} [${n.edge_type ?? "connected"}] (hop ${n.hop})
|
|
2080
|
+
`;
|
|
2081
|
+
}
|
|
2082
|
+
response += "\n";
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
const allEntityIds = [primaryEntity.entity_id, ...neighbors.slice(0, 30).map((n) => n.entity_id)];
|
|
2086
|
+
const embedding = await getEmbedding(entity);
|
|
2087
|
+
const chunks = await qdrant.search(config.qdrant.collectionName, {
|
|
2088
|
+
vector: embedding,
|
|
2089
|
+
limit,
|
|
2090
|
+
filter: {
|
|
2091
|
+
must: [{ key: "entity_ids", match: { any: allEntityIds } }]
|
|
2092
|
+
},
|
|
2093
|
+
with_payload: true,
|
|
2094
|
+
score_threshold: 0.1
|
|
2095
|
+
}).catch(() => []);
|
|
2096
|
+
if (chunks.length > 0) {
|
|
2097
|
+
response += `**Related documents (${chunks.length}):**
|
|
2098
|
+
|
|
2099
|
+
`;
|
|
2100
|
+
for (const c of chunks.slice(0, limit)) {
|
|
2101
|
+
const p = c.payload;
|
|
2102
|
+
const sourceType = p.source_type || "";
|
|
2103
|
+
const sourceId = p.source_id || "";
|
|
2104
|
+
const content = p.content || "";
|
|
2105
|
+
response += `### [${sourceType.toUpperCase()}] ${sourceId}
|
|
2106
|
+
`;
|
|
2107
|
+
if (p.source_url)
|
|
2108
|
+
response += `URL: ${p.source_url}
|
|
2109
|
+
`;
|
|
2110
|
+
response += `
|
|
2111
|
+
${content.substring(0, 600)}${content.length > 600 ? "..." : ""}
|
|
2112
|
+
|
|
2113
|
+
---
|
|
2114
|
+
|
|
2115
|
+
`;
|
|
2116
|
+
}
|
|
2117
|
+
} else {
|
|
2118
|
+
response += `*No indexed documents found for this entity. Run the graph indexer first.*
|
|
2119
|
+
`;
|
|
2120
|
+
}
|
|
2121
|
+
return response;
|
|
2122
|
+
}
|
|
2123
|
+
async function handleImpactAnalysis(args) {
|
|
2124
|
+
const { target, max_depth = 3 } = args;
|
|
2125
|
+
if (!pgAvailable) {
|
|
2126
|
+
return `PostgreSQL not available. Cannot perform graph impact analysis. Use search_as_architect instead.`;
|
|
2127
|
+
}
|
|
2128
|
+
let entities = [];
|
|
2129
|
+
try {
|
|
2130
|
+
entities = await searchGraphEntities(target, void 0, 3);
|
|
2131
|
+
} catch {
|
|
2132
|
+
}
|
|
2133
|
+
if (entities.length === 0) {
|
|
2134
|
+
return `Entity "${target}" not found in knowledge graph. Try running the graph builder first (GRAPH_ENABLED=true).`;
|
|
2135
|
+
}
|
|
2136
|
+
const primary = entities[0];
|
|
2137
|
+
let response = `## Impact Analysis: "${target}"
|
|
2138
|
+
|
|
2139
|
+
`;
|
|
2140
|
+
response += `**Analyzing:** ${primary.name} (${primary.entity_type})
|
|
2141
|
+
|
|
2142
|
+
`;
|
|
2143
|
+
let neighbors = [];
|
|
2144
|
+
try {
|
|
2145
|
+
neighbors = await getGraphNeighbors([primary.entity_id], max_depth);
|
|
2146
|
+
} catch {
|
|
2147
|
+
}
|
|
2148
|
+
if (neighbors.length === 0) {
|
|
2149
|
+
response += `No connected entities found. Graph may not be built yet.`;
|
|
2150
|
+
return response;
|
|
2151
|
+
}
|
|
2152
|
+
const byType = /* @__PURE__ */ new Map();
|
|
2153
|
+
for (const n of neighbors) {
|
|
2154
|
+
const group = byType.get(n.entity_type) ?? [];
|
|
2155
|
+
group.push(n);
|
|
2156
|
+
byType.set(n.entity_type, group);
|
|
2157
|
+
}
|
|
2158
|
+
response += `**Total affected entities: ${neighbors.length}**
|
|
2159
|
+
|
|
2160
|
+
`;
|
|
2161
|
+
response += `| Entity Type | Count | Relationship |
|
|
2162
|
+
`;
|
|
2163
|
+
response += `|-------------|-------|--------------|
|
|
2164
|
+
`;
|
|
2165
|
+
for (const [type, nodes] of byType) {
|
|
2166
|
+
const edgeTypes = [...new Set(nodes.map((n) => n.edge_type).filter(Boolean))].join(", ");
|
|
2167
|
+
response += `| ${type} | ${nodes.length} | ${edgeTypes || "various"} |
|
|
2168
|
+
`;
|
|
2169
|
+
}
|
|
2170
|
+
response += "\n";
|
|
2171
|
+
for (const [type, nodes] of byType) {
|
|
2172
|
+
response += `**${type.toUpperCase()} (${nodes.length}):**
|
|
2173
|
+
`;
|
|
2174
|
+
for (const n of nodes.slice(0, 15)) {
|
|
2175
|
+
response += ` - ${n.name} via ${n.edge_type ?? "connected"} (${n.hop} hop${n.hop > 1 ? "s" : ""})
|
|
2176
|
+
`;
|
|
2177
|
+
}
|
|
2178
|
+
if (nodes.length > 15)
|
|
2179
|
+
response += ` ... and ${nodes.length - 15} more
|
|
2180
|
+
`;
|
|
2181
|
+
response += "\n";
|
|
2182
|
+
}
|
|
2183
|
+
return response;
|
|
2184
|
+
}
|
|
2185
|
+
function parseTimeRange(timeRange) {
|
|
2186
|
+
const match = timeRange.match(/^(\d+)([mhd])$/);
|
|
2187
|
+
if (!match)
|
|
2188
|
+
return 60 * 60 * 1e3;
|
|
2189
|
+
const [, value, unit] = match;
|
|
2190
|
+
const num = parseInt(value);
|
|
2191
|
+
switch (unit) {
|
|
2192
|
+
case "m":
|
|
2193
|
+
return num * 60 * 1e3;
|
|
2194
|
+
case "h":
|
|
2195
|
+
return num * 60 * 60 * 1e3;
|
|
2196
|
+
case "d":
|
|
2197
|
+
return num * 24 * 60 * 60 * 1e3;
|
|
2198
|
+
default:
|
|
2199
|
+
return 60 * 60 * 1e3;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
async function handleSearchDatadogLogs(args) {
|
|
2203
|
+
if (!config.datadog.enabled) {
|
|
2204
|
+
return "Datadog integration is disabled. Set DATADOG_ENABLED=true with DATADOG_API_KEY and DATADOG_APP_KEY to enable.";
|
|
2205
|
+
}
|
|
2206
|
+
if (!config.datadog.apiKey || !config.datadog.appKey) {
|
|
2207
|
+
return "Datadog API keys not configured. Set DATADOG_API_KEY and DATADOG_APP_KEY environment variables.";
|
|
2208
|
+
}
|
|
2209
|
+
let ddQuery = args.query;
|
|
2210
|
+
if (args.service) {
|
|
2211
|
+
ddQuery += ` service:${args.service}`;
|
|
2212
|
+
}
|
|
2213
|
+
if (args.env) {
|
|
2214
|
+
ddQuery += ` env:${args.env}`;
|
|
2215
|
+
}
|
|
2216
|
+
const limit = Math.min(args.limit || 30, 100);
|
|
2217
|
+
const timeRangeMs = parseTimeRange(args.time_range || "1h");
|
|
2218
|
+
const now = /* @__PURE__ */ new Date();
|
|
2219
|
+
const from = new Date(now.getTime() - timeRangeMs);
|
|
2220
|
+
try {
|
|
2221
|
+
const baseUrl = `https://api.${config.datadog.site}`;
|
|
2222
|
+
const response = await fetch(`${baseUrl}/api/v2/logs/events/search`, {
|
|
2223
|
+
method: "POST",
|
|
2224
|
+
headers: {
|
|
2225
|
+
"DD-API-KEY": config.datadog.apiKey,
|
|
2226
|
+
"DD-APPLICATION-KEY": config.datadog.appKey,
|
|
2227
|
+
"Content-Type": "application/json"
|
|
2228
|
+
},
|
|
2229
|
+
body: JSON.stringify({
|
|
2230
|
+
filter: {
|
|
2231
|
+
query: ddQuery,
|
|
2232
|
+
from: from.toISOString(),
|
|
2233
|
+
to: now.toISOString()
|
|
2234
|
+
},
|
|
2235
|
+
sort: "-timestamp",
|
|
2236
|
+
page: { limit }
|
|
2237
|
+
})
|
|
2238
|
+
});
|
|
2239
|
+
if (!response.ok) {
|
|
2240
|
+
const errorText = await response.text();
|
|
2241
|
+
return `Datadog API error (${response.status}): ${errorText}`;
|
|
2242
|
+
}
|
|
2243
|
+
const data = await response.json();
|
|
2244
|
+
if (!data.data || data.data.length === 0) {
|
|
2245
|
+
return `No logs found for query: \`${ddQuery}\` (time range: ${args.time_range || "1h"})`;
|
|
2246
|
+
}
|
|
2247
|
+
let result = `## Datadog Logs
|
|
2248
|
+
|
|
2249
|
+
`;
|
|
2250
|
+
result += `**Query:** \`${ddQuery}\`
|
|
2251
|
+
`;
|
|
2252
|
+
result += `**Time range:** ${from.toISOString()} \u2192 ${now.toISOString()}
|
|
2253
|
+
`;
|
|
2254
|
+
result += `**Results:** ${data.data.length}${data.meta?.page?.after ? " (more available)" : ""}
|
|
2255
|
+
|
|
2256
|
+
`;
|
|
2257
|
+
for (const log of data.data) {
|
|
2258
|
+
const attrs = log.attributes;
|
|
2259
|
+
const ts = attrs.timestamp ? new Date(attrs.timestamp).toISOString() : "unknown";
|
|
2260
|
+
const status = (attrs.status || "info").toUpperCase();
|
|
2261
|
+
const service = attrs.service || "unknown";
|
|
2262
|
+
const host = attrs.host || "";
|
|
2263
|
+
result += `### ${status} | ${service}${host ? ` @ ${host}` : ""}
|
|
2264
|
+
`;
|
|
2265
|
+
result += `**Time:** ${ts}
|
|
2266
|
+
`;
|
|
2267
|
+
if (attrs.message) {
|
|
2268
|
+
const msg = attrs.message.length > 1e3 ? attrs.message.substring(0, 1e3) + "..." : attrs.message;
|
|
2269
|
+
result += `\`\`\`
|
|
2270
|
+
${msg}
|
|
2271
|
+
\`\`\`
|
|
2272
|
+
`;
|
|
2273
|
+
}
|
|
2274
|
+
if (attrs.tags && attrs.tags.length > 0) {
|
|
2275
|
+
result += `**Tags:** ${attrs.tags.slice(0, 10).join(", ")}
|
|
2276
|
+
`;
|
|
2277
|
+
}
|
|
2278
|
+
result += "\n---\n\n";
|
|
2279
|
+
}
|
|
2280
|
+
return result;
|
|
2281
|
+
} catch (error) {
|
|
2282
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2283
|
+
return `Failed to query Datadog: ${message}`;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
1744
2286
|
async function main() {
|
|
1745
2287
|
await ensureCollection();
|
|
1746
2288
|
await initPostgres();
|
|
@@ -1781,6 +2323,9 @@ async function main() {
|
|
|
1781
2323
|
case "get_db_stats":
|
|
1782
2324
|
result = await handleGetDbStats();
|
|
1783
2325
|
break;
|
|
2326
|
+
case "raw_sql":
|
|
2327
|
+
result = await handleRawSql(args);
|
|
2328
|
+
break;
|
|
1784
2329
|
case "search_as_analyst":
|
|
1785
2330
|
result = await handleSearchAsAnalyst(args);
|
|
1786
2331
|
break;
|
|
@@ -1820,6 +2365,18 @@ async function main() {
|
|
|
1820
2365
|
case "get_index_stats":
|
|
1821
2366
|
result = await handleGetStats();
|
|
1822
2367
|
break;
|
|
2368
|
+
case "graph_search":
|
|
2369
|
+
result = await handleGraphSearch(args);
|
|
2370
|
+
break;
|
|
2371
|
+
case "entity_search":
|
|
2372
|
+
result = await handleEntitySearch(args);
|
|
2373
|
+
break;
|
|
2374
|
+
case "impact_analysis":
|
|
2375
|
+
result = await handleImpactAnalysis(args);
|
|
2376
|
+
break;
|
|
2377
|
+
case "search_datadog_logs":
|
|
2378
|
+
result = await handleSearchDatadogLogs(args);
|
|
2379
|
+
break;
|
|
1823
2380
|
default:
|
|
1824
2381
|
throw new Error(`Unknown tool: ${name}`);
|
|
1825
2382
|
}
|
|
@@ -1833,6 +2390,7 @@ async function main() {
|
|
|
1833
2390
|
await server.connect(transport);
|
|
1834
2391
|
const pgStatus = pgAvailable ? "connected" : "disabled";
|
|
1835
2392
|
const codeSearchStatus = config.codeSearchApi.enabled ? config.codeSearchApi.url : "disabled";
|
|
1836
|
-
|
|
2393
|
+
const datadogStatus = config.datadog.enabled ? config.datadog.site : "disabled";
|
|
2394
|
+
console.error(`Domain RAG MCP Server v3.1 started (embedding: ${embeddingProvider.name}, qdrant: ${config.qdrant.url}, postgres: ${pgStatus}, code-search: ${codeSearchStatus}, datadog: ${datadogStatus})`);
|
|
1837
2395
|
}
|
|
1838
2396
|
main().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,46 +1,47 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "domain-rag-mcp-server",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "MCP server for domain RAG search — connects to Qdrant + PostgreSQL + Code Search API for hybrid search across Jira, Confluence, Git commits, and server-side code repositories",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.mjs",
|
|
7
|
-
"bin": {
|
|
8
|
-
"domain-rag-mcp-server": "dist/index.mjs"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"dist/index.mjs"
|
|
12
|
-
],
|
|
13
|
-
"scripts": {
|
|
14
|
-
"build": "tsc",
|
|
15
|
-
"build:npm": "node build.mjs",
|
|
16
|
-
"start": "node dist/index.mjs",
|
|
17
|
-
"dev": "tsx src/index.ts"
|
|
18
|
-
},
|
|
19
|
-
"keywords": [
|
|
20
|
-
"mcp",
|
|
21
|
-
"rag",
|
|
22
|
-
"qdrant",
|
|
23
|
-
"postgresql",
|
|
24
|
-
"jira",
|
|
25
|
-
"confluence",
|
|
26
|
-
"git",
|
|
27
|
-
"semantic-search",
|
|
28
|
-
"claude",
|
|
29
|
-
"cursor"
|
|
30
|
-
],
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"@
|
|
33
|
-
"@
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"@types/
|
|
41
|
-
"@types/
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "domain-rag-mcp-server",
|
|
3
|
+
"version": "3.3.0",
|
|
4
|
+
"description": "MCP server for domain RAG search — connects to Qdrant + PostgreSQL + Code Search API for hybrid search across Jira, Confluence, Git commits, and server-side code repositories",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.mjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"domain-rag-mcp-server": "dist/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/index.mjs"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"build:npm": "node build.mjs",
|
|
16
|
+
"start": "node dist/index.mjs",
|
|
17
|
+
"dev": "tsx src/index.ts"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"rag",
|
|
22
|
+
"qdrant",
|
|
23
|
+
"postgresql",
|
|
24
|
+
"jira",
|
|
25
|
+
"confluence",
|
|
26
|
+
"git",
|
|
27
|
+
"semantic-search",
|
|
28
|
+
"claude",
|
|
29
|
+
"cursor"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@domain-rag/shared": "file:../shared",
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
34
|
+
"@qdrant/js-client-rest": "^1.7.0",
|
|
35
|
+
"dotenv": "^16.3.1",
|
|
36
|
+
"pg": "^8.11.3",
|
|
37
|
+
"uuid": "^9.0.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^20.10.0",
|
|
41
|
+
"@types/pg": "^8.10.9",
|
|
42
|
+
"@types/uuid": "^9.0.7",
|
|
43
|
+
"esbuild": "^0.20.2",
|
|
44
|
+
"tsx": "^4.7.0",
|
|
45
|
+
"typescript": "^5.3.3"
|
|
46
|
+
}
|
|
47
|
+
}
|