arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
138 lines • 5.57 kB
JavaScript
import process from "node:process";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { search, ensureOllamaInstalled, ensureModelAvailable, isOllamaRunning, startOllamaServer } from "../rag/index.js";
import { createRequire } from "module";
import { searchEnforcer } from "./search-enforcer.js";
const require = createRequire(import.meta.url);
const pkg = require("../../package.json");
const PACKAGE_VERSION = pkg.version ?? "0.0.0";
const DEFAULT_TOP_K = 5;
function formatResultText(payload) {
const header = [`Query: ${payload.query}`, `Matches: ${payload.results.length}`, `Latency: ${payload.tookMs}ms`].join(" | ");
if (!payload.results.length) {
return `${header}\n(No matching chunks found in the local index)`;
}
const body = payload.results
.map((item, index) => {
const snippet = item.chunk.trim().replace(/\s+/g, " ");
const truncated = snippet.length <= 320 ? snippet : `${snippet.slice(0, 320)}…`;
return `${index + 1}. ${item.file} [score=${item.score.toFixed(4)}]\n ${truncated}`;
})
.join("\n\n");
return `${header}\n\n${body}`;
}
export function createArelaMcpServer(options = {}) {
const server = new McpServer({
name: "arela-rag",
version: PACKAGE_VERSION,
});
const ragConfig = {
cwd: options.cwd ?? process.cwd(),
model: options.model,
ollamaHost: options.ollamaHost,
};
const defaultTopK = options.defaultTopK ?? DEFAULT_TOP_K;
server.registerTool("arela_search", {
title: "Arela Semantic Search",
description: "Search the local Arela RAG index for relevant files and chunks.",
inputSchema: {
query: z.string().min(3, "Provide a more descriptive query."),
topK: z.number().int().min(1).max(20).optional().describe("Maximum number of chunks to return"),
},
outputSchema: {
query: z.string(),
tookMs: z.number(),
results: z.array(z.object({
file: z.string(),
score: z.number(),
chunk: z.string(),
})),
},
}, async ({ query, topK }) => {
const start = Date.now();
// Record arela_search usage
searchEnforcer.recordToolCall('arela_search', { query, topK });
try {
const results = await search(query, ragConfig, topK ?? defaultTopK);
const payload = {
query,
tookMs: Date.now() - start,
results: results.map((item) => ({
file: item.file,
chunk: item.chunk,
score: Number(item.score.toFixed(6)),
})),
};
return {
content: [{ type: "text", text: formatResultText(payload) }],
structuredContent: payload,
};
}
catch (error) {
const payload = {
query,
tookMs: Date.now() - start,
results: [],
};
const message = error.message ?? "Unknown error";
return {
content: [{ type: "text", text: `Search failed: ${message}` }],
structuredContent: payload,
isError: true,
};
}
});
// Register grep_search wrapper that enforces arela_search first
server.registerTool("grep_search", {
title: "Grep Search (Enforced)",
description: "Search files using grep. NOTE: You MUST try arela_search first!",
inputSchema: {
query: z.string().min(1, "Provide a search query"),
path: z.string().optional().describe("Path to search in"),
},
outputSchema: {
allowed: z.boolean(),
message: z.string(),
},
}, async ({ query, path }) => {
// Record grep attempt
searchEnforcer.recordToolCall('grep_search', { query, path });
// Validate if grep is allowed
const validation = searchEnforcer.validateGrepAttempt(query);
if (!validation.allowed) {
// BLOCKED! Return error message
return {
content: [{ type: "text", text: validation.message }],
structuredContent: {
allowed: false,
message: validation.message
},
isError: true,
};
}
// Allowed - they tried arela_search first
return {
content: [{ type: "text", text: "✅ grep_search allowed (you tried arela_search first). Proceed with your grep command." }],
structuredContent: {
allowed: true,
message: "Allowed - arela_search was tried first"
},
};
});
return server;
}
export async function runArelaMcpServer(options = {}) {
const { cwd = process.cwd(), model = "nomic-embed-text", ollamaHost = "http://localhost:11434" } = options;
// Ensure Ollama is installed and running with the required model
await ensureOllamaInstalled();
if (!(await isOllamaRunning(ollamaHost))) {
await startOllamaServer();
}
await ensureModelAvailable(model, ollamaHost);
const server = createArelaMcpServer(options);
const transport = new StdioServerTransport();
await server.connect(transport);
}
//# sourceMappingURL=server.js.map