@twahaa/codefinder 1.0.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/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025, CodeFinder contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ This repo contains an MCP server (tool provider) and a sample Gemini-based MCP client with an interactive TUI.
2
+ The server exposes safe, deterministic capabilities; the client handles reasoning and tool orchestration.
3
+
4
+ ---
5
+
6
+ ## Security Model
7
+
8
+ ### Path Guard
9
+
10
+ All filesystem access is restricted to a fixed project root (`target-codebase`).
11
+
12
+ Every user-supplied path is validated using a path guard that:
13
+
14
+ - Resolves paths against the project root
15
+ - Rejects absolute paths
16
+ - Resolves symlinks using `realpath`
17
+ - Ensures the final resolved path cannot escape the sandbox
18
+
19
+ This prevents:
20
+
21
+ - `../` path traversal attacks
22
+ - Symlink-based escapes
23
+ - Accidental access to system files (e.g. `/etc`, `/home`)
24
+
25
+ ---
26
+
27
+ ### Resource Limits
28
+
29
+ To prevent excessive resource usage and prompt flooding, the server enforces hard limits:
30
+
31
+ - Maximum file size for reads
32
+ - Maximum line count for files
33
+ - Maximum directory traversal depth
34
+ - Maximum search results
35
+
36
+ These limits are enforced **server-side** and cannot be overridden by clients.
37
+
38
+ ---
39
+
40
+ ## Tools
41
+
42
+ ### `list_files`
43
+
44
+ Lists the structure of a directory within the project.
45
+
46
+ **Purpose**
47
+
48
+ - Provides orientation within the codebase
49
+ - Helps decide where to search or inspect next
50
+
51
+ **Input**
52
+
53
+ - `dir` (optional, project-relative path, defaults to project root)
54
+
55
+ **Output**
56
+
57
+ - Structured directory entries (files and directories)
58
+ - Nested up to a fixed maximum depth
59
+
60
+ **Notes**
61
+
62
+ - Common large or irrelevant directories are ignored (e.g. `node_modules`, `.git`, build outputs)
63
+
64
+ ---
65
+
66
+ ### `search_code`
67
+
68
+ Searches for a plain-text query across the project.
69
+
70
+ **Purpose**
71
+
72
+ - Quickly locate where a concept, identifier, or keyword appears
73
+ - Avoid reading unnecessary files
74
+
75
+ **Input**
76
+
77
+ - `query` (required string)
78
+ - `dir` (optional starting directory, project-relative)
79
+
80
+ **Output**
81
+
82
+ - Project-relative file path
83
+ - Line number
84
+ - Short line preview
85
+
86
+ **Constraints**
87
+
88
+ - Plain-text search (no regex)
89
+ - File size limits enforced
90
+ - Total and per-file match limits enforced
91
+
92
+ ---
93
+
94
+ ### `read_file`
95
+
96
+ Reads the contents of a single file.
97
+
98
+ **Purpose**
99
+
100
+ - Inspect specific implementation details after locating a file
101
+
102
+ **Input**
103
+
104
+ - `path` (project-relative file path)
105
+
106
+ **Constraints**
107
+
108
+ - Path must point to a file (directories rejected)
109
+ - File size and line limits enforced
110
+ - Binary or unreadable files are rejected
111
+ - Access to package manager manifests (`package.json`, `package-lock.json`) is denied
112
+
113
+ ---
114
+
115
+ ## Typical LLM Workflow
116
+
117
+ A typical interaction with this MCP server looks like:
118
+
119
+ 1. Call `list_files` to understand the project structure
120
+ 2. Call `search_code` to locate relevant files or identifiers
121
+ 3. Call `read_file` to inspect specific code sections
122
+
123
+ The server intentionally avoids higher-level reasoning and only provides safe primitives that enable the LLM to reason effectively.
124
+
125
+ ---
126
+
127
+ ## Running the Server
128
+
129
+ ### Prerequisites
130
+
131
+ - Node.js 18+
132
+ - npm
133
+
134
+ ### Install dependencies
135
+
136
+ ```bash
137
+ npm install
138
+ ```
139
+
140
+ ### Run the server (dev)
141
+
142
+ ```bash
143
+ npm run server:dev
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Running the Gemini Client
149
+
150
+ ### Prerequisites
151
+
152
+ - Node.js 18+
153
+ - Gemini API key (`GEMINI_API_KEY` env or enter at prompt)
154
+
155
+ ### Start the client
156
+
157
+ ```bash
158
+ npm run client:dev
159
+ ```
160
+
161
+ You will be prompted for:
162
+
163
+ - Gemini API key (or use `GEMINI_API_KEY`)
164
+ - Gemini model (choices):
165
+ - `gemini-2.5-flash-lite` (fast, generous free tier)
166
+ - `gemini-2.5-flash` (balanced, stricter free limits)
167
+ - `gemini-2.0-flash-lite` (legacy, stable)
168
+
169
+ ### Client behavior
170
+
171
+ - Shows a figlet banner and colored prompts
172
+ - Lists available MCP tools
173
+ - Chat loop with tool-call handling
174
+ - Graceful handling of quota/rate-limit exhaustion (shows a note to retry or change model)
175
+
176
+ ---
177
+
178
+ ## What the Server Provides (Tools)
179
+
180
+ The MCP server exposes three safe, deterministic tools:
181
+
182
+ - `list_files`: Explore directories within the sandboxed project root (ignores large/irrelevant folders and enforces max depth).
183
+ - `search_code`: Plain-text search with result caps and file size limits.
184
+ - `read_file`: Read a single file with size/line limits; rejects binaries and denies `package.json` / `package-lock.json`.
185
+
186
+ See the sections above for full inputs/outputs and constraints.
187
+
188
+ ---
@@ -0,0 +1,37 @@
1
+ import { searchCode } from "../tools/searchCode.js";
2
+ function printUsage() {
3
+ console.log(`
4
+ Usage:
5
+ search <query> [directory]
6
+
7
+ Examples:
8
+ search TODO
9
+ search "import fs" src
10
+ `);
11
+ }
12
+ const args = process.argv.slice(2);
13
+ if (args.length === 0) {
14
+ printUsage();
15
+ process.exit(1);
16
+ }
17
+ const query = args[0];
18
+ const dir = args[1];
19
+ try {
20
+ const results = searchCode(query, dir);
21
+ if (results.length === 0) {
22
+ console.log("No matches found.");
23
+ process.exit(0);
24
+ }
25
+ for (const r of results) {
26
+ console.log(`${r.filePath}:${r.lineNumber} ${r.preview}`);
27
+ }
28
+ }
29
+ catch (err) {
30
+ if (err instanceof Error) {
31
+ console.error("Error:", err.message);
32
+ }
33
+ else {
34
+ console.error("Unknown error");
35
+ }
36
+ process.exit(1);
37
+ }
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ import { config } from "dotenv";
3
+ config();
4
+ import { intro, outro, text, note, spinner, isCancel, select, } from "@clack/prompts";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { SchemaType, GoogleGenerativeAI, } from "@google/generative-ai";
8
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
10
+ import { logError, logModelText, logToolCall, logToolResult, renderLogo, } from "./ui/openTUI.js";
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const builtServerPath = path.resolve(__dirname, "server.js");
13
+ renderLogo();
14
+ intro("CodeFinder by TWAHaa");
15
+ note("MCP + Gemini Client");
16
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
17
+ console.error("This CLI requires a TTY. Please run in an interactive terminal.");
18
+ process.exit(1);
19
+ }
20
+ const transport = new StdioClientTransport({
21
+ command: process.execPath,
22
+ args: [builtServerPath],
23
+ });
24
+ const mcpClient = new Client({
25
+ name: "CodeFinder",
26
+ version: "1.0.0",
27
+ });
28
+ await mcpClient.connect(transport);
29
+ const apiKeyInput = await text({
30
+ message: "Enter Gemini API key (leave blank to use GEMINI_API_KEY env):",
31
+ placeholder: "AIza...",
32
+ });
33
+ if (isCancel(apiKeyInput)) {
34
+ outro("Cancelled.");
35
+ process.exit(0);
36
+ }
37
+ const geminiApiKey = (typeof apiKeyInput === "string" ? apiKeyInput.trim() : "") ||
38
+ process.env.GEMINI_API_KEY;
39
+ if (!geminiApiKey) {
40
+ console.error("A Gemini API key is required. Set env or enter it at prompt.");
41
+ process.exit(1);
42
+ }
43
+ const tools = await mcpClient.listTools();
44
+ note(tools.tools.length
45
+ ? `Tools: ${tools.tools.map((t) => t.name).join(", ")}`
46
+ : "No tools available.");
47
+ const geminiTools = tools.tools.map((tool) => {
48
+ const inputSchema = tool.inputSchema;
49
+ const properties = inputSchema &&
50
+ typeof inputSchema === "object" &&
51
+ "properties" in inputSchema
52
+ ? (inputSchema.properties ??
53
+ {})
54
+ : {};
55
+ const required = inputSchema && typeof inputSchema === "object" && "required" in inputSchema
56
+ ? inputSchema.required
57
+ : undefined;
58
+ return {
59
+ name: tool.name,
60
+ description: tool.description ?? "",
61
+ parameters: {
62
+ type: SchemaType.OBJECT,
63
+ properties,
64
+ required,
65
+ },
66
+ };
67
+ });
68
+ const modelChoice = await select({
69
+ message: "Choose a Gemini model",
70
+ options: [
71
+ {
72
+ value: "gemini-2.5-flash-lite",
73
+ label: "Gemini 2.5 Flash-Lite (fast, generous free tier)",
74
+ },
75
+ {
76
+ value: "gemini-2.5-flash",
77
+ label: "Gemini 2.5 Flash (balanced, stricter free limits)",
78
+ },
79
+ {
80
+ value: "gemini-2.0-flash-lite",
81
+ label: "Gemini 2.0 Flash / Flash-Lite (legacy, stable)",
82
+ },
83
+ ],
84
+ initialValue: "gemini-2.5-flash-lite",
85
+ });
86
+ if (isCancel(modelChoice)) {
87
+ outro("Cancelled.");
88
+ process.exit(0);
89
+ }
90
+ const modelName = typeof modelChoice === "string" ? modelChoice : "gemini-2.5-flash-lite";
91
+ note(`Using model: ${modelName}`);
92
+ const genAI = new GoogleGenerativeAI(geminiApiKey);
93
+ const model = genAI.getGenerativeModel({
94
+ model: modelName,
95
+ tools: [
96
+ {
97
+ functionDeclarations: geminiTools,
98
+ },
99
+ ],
100
+ });
101
+ const chat = model.startChat({ history: [] });
102
+ function isResourceExhausted(err) {
103
+ const anyErr = err;
104
+ const msg = anyErr?.message?.toLowerCase?.() ?? "";
105
+ const statusText = anyErr?.statusText?.toLowerCase?.() ?? "";
106
+ return (anyErr?.status === 429 ||
107
+ statusText.includes("too many") ||
108
+ msg.includes("resource has been exhausted") ||
109
+ msg.includes("quota") ||
110
+ msg.includes("rate limit"));
111
+ }
112
+ async function handleTurn(userText) {
113
+ let result;
114
+ try {
115
+ result = await chat.sendMessage(userText);
116
+ }
117
+ catch (err) {
118
+ if (isResourceExhausted(err)) {
119
+ note("Resource/quota exhausted for this model. Try again later or pick another model.");
120
+ return;
121
+ }
122
+ throw err;
123
+ }
124
+ let response = result.response;
125
+ // Continue handling tool calls until the model stops requesting them
126
+ while (true) {
127
+ const call = response.functionCalls()?.[0];
128
+ if (!call) {
129
+ logModelText(response.text());
130
+ return;
131
+ }
132
+ const { name, args } = call;
133
+ const parsedArgs = args && typeof args === "object"
134
+ ? args
135
+ : {};
136
+ logToolCall(name, parsedArgs);
137
+ const toolResult = await mcpClient.callTool({
138
+ name,
139
+ arguments: parsedArgs,
140
+ });
141
+ logToolResult(toolResult.content);
142
+ let followUp;
143
+ try {
144
+ followUp = await chat.sendMessage([
145
+ {
146
+ functionResponse: {
147
+ name,
148
+ response: { content: toolResult.content },
149
+ },
150
+ },
151
+ ]);
152
+ }
153
+ catch (err) {
154
+ if (isResourceExhausted(err)) {
155
+ note("Resource/quota exhausted while sending tool result. Try again later or pick another model.");
156
+ return;
157
+ }
158
+ throw err;
159
+ }
160
+ response = followUp.response;
161
+ }
162
+ }
163
+ console.log("Chat ready. Type your question (or 'exit' to quit).");
164
+ while (true) {
165
+ const queryInput = await text({
166
+ message: "You",
167
+ placeholder: "Ask me anything (type 'exit' to quit)",
168
+ });
169
+ if (isCancel(queryInput)) {
170
+ outro("Goodbye!");
171
+ break;
172
+ }
173
+ const query = typeof queryInput === "string" ? queryInput.trim() : String(queryInput);
174
+ if (!query)
175
+ continue;
176
+ if (query.toLowerCase() === "exit") {
177
+ outro("Goodbye!");
178
+ break;
179
+ }
180
+ try {
181
+ const spin = spinner();
182
+ spin.start("Sending to Gemini...");
183
+ await handleTurn(query);
184
+ spin.stop("Done.");
185
+ }
186
+ catch (err) {
187
+ logError(err);
188
+ }
189
+ }
@@ -0,0 +1,122 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import z from "zod";
4
+ import { searchCode } from "./tools/searchCode.js";
5
+ import { readFile } from "./tools/readFile.js";
6
+ import { listFile } from "./tools/listFile.js";
7
+ const server = new McpServer({
8
+ name: "CodeFinder",
9
+ version: "1.0.0",
10
+ }, {
11
+ capabilities: {
12
+ tools: {},
13
+ resources: {},
14
+ prompts: {},
15
+ },
16
+ });
17
+ server.registerTool("search_code", {
18
+ description: "Search for a text query inside a directory",
19
+ inputSchema: {
20
+ query: z.string().min(1).describe("Text to search for"),
21
+ dir: z.string().optional().describe("Relative directory path"),
22
+ },
23
+ }, async ({ query, dir }) => {
24
+ try {
25
+ const results = searchCode(query, dir);
26
+ return {
27
+ content: [
28
+ {
29
+ type: "text",
30
+ text: JSON.stringify(results, null, 2),
31
+ },
32
+ ],
33
+ };
34
+ }
35
+ catch (err) {
36
+ return {
37
+ content: [
38
+ {
39
+ type: "text",
40
+ text: JSON.stringify({
41
+ error: err instanceof Error
42
+ ? `Error: ${err.message}`
43
+ : "Unknown error",
44
+ }),
45
+ },
46
+ ],
47
+ };
48
+ }
49
+ });
50
+ server.registerTool("list_files", {
51
+ description: "List the files in a directory",
52
+ inputSchema: {
53
+ path: z.string().min(0).optional().describe("Relative path to the directory"),
54
+ },
55
+ }, async ({ path }) => {
56
+ try {
57
+ const files = listFile(path);
58
+ return {
59
+ content: [
60
+ {
61
+ type: "text",
62
+ text: JSON.stringify(files, null, 2),
63
+ },
64
+ ],
65
+ };
66
+ }
67
+ catch (err) {
68
+ return {
69
+ content: [
70
+ {
71
+ type: "text",
72
+ text: JSON.stringify({
73
+ error: err instanceof Error
74
+ ? `Error: ${err.message}`
75
+ : "Unknown error",
76
+ }),
77
+ },
78
+ ],
79
+ };
80
+ }
81
+ });
82
+ server.registerTool("read_file", {
83
+ description: "Read the contents of a text file safely",
84
+ inputSchema: {
85
+ path: z.string().min(1).describe("Relative path to the file"),
86
+ },
87
+ }, async ({ path }) => {
88
+ try {
89
+ const content = readFile(path);
90
+ return {
91
+ content: [
92
+ {
93
+ type: "text",
94
+ text: content,
95
+ },
96
+ ],
97
+ };
98
+ }
99
+ catch (err) {
100
+ return {
101
+ content: [
102
+ {
103
+ type: "text",
104
+ text: JSON.stringify({
105
+ error: err instanceof Error
106
+ ? `Error: ${err.message}`
107
+ : "Unknown error",
108
+ }),
109
+ },
110
+ ],
111
+ };
112
+ }
113
+ });
114
+ async function main() {
115
+ const transport = new StdioServerTransport();
116
+ await server.connect(transport);
117
+ console.error("MCP server running");
118
+ }
119
+ main().catch((err) => {
120
+ console.error("Failed to start MCP server:", err);
121
+ process.exit(1);
122
+ });
@@ -0,0 +1,50 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { resolveSafePath } from "../utils/pathGuard.js";
4
+ const IGNORE = new Set([
5
+ "node_modules",
6
+ ".git",
7
+ "dist",
8
+ "build",
9
+ ".next",
10
+ ".env",
11
+ "package-lock.json",
12
+ "yarn.lock",
13
+ "pnpm-lock.yaml",
14
+ "package.json",
15
+ ]);
16
+ const MAX_DEPTH = 2;
17
+ export function listFile(dir = ".") {
18
+ const safeDir = resolveSafePath(dir);
19
+ const stats = fs.statSync(safeDir);
20
+ if (!stats.isDirectory()) {
21
+ throw new Error("Path must be a directory");
22
+ }
23
+ return walkDirectory(safeDir, 0);
24
+ }
25
+ function walkDirectory(absoluteDir, depth) {
26
+ const entires = fs.readdirSync(absoluteDir, { withFileTypes: true });
27
+ const result = [];
28
+ for (const entry of entires) {
29
+ if (IGNORE.has(entry.name))
30
+ continue;
31
+ const entryPath = path.join(absoluteDir, entry.name);
32
+ if (entry.isFile()) {
33
+ result.push({
34
+ name: entry.name,
35
+ type: "file",
36
+ });
37
+ }
38
+ if (entry.isDirectory()) {
39
+ const dirEntry = {
40
+ name: entry.name,
41
+ type: "directory",
42
+ };
43
+ if (depth < MAX_DEPTH) {
44
+ dirEntry.children = walkDirectory(entryPath, depth + 1);
45
+ }
46
+ result.push(dirEntry);
47
+ }
48
+ }
49
+ return result;
50
+ }
@@ -0,0 +1,26 @@
1
+ import fs from "fs";
2
+ import { resolveSafePath } from "../utils/pathGuard.js";
3
+ export function readFile(userPath) {
4
+ const safePath = resolveSafePath(userPath);
5
+ const stats = fs.statSync(safePath);
6
+ const MAX_SIZE = 200 * 1024;
7
+ const MAX_LINES = 1000;
8
+ if (!stats.isFile()) {
9
+ throw new Error("The path should specify a file not a directory");
10
+ }
11
+ if (stats.size > MAX_SIZE) {
12
+ throw new Error("File too big");
13
+ }
14
+ let content;
15
+ try {
16
+ content = fs.readFileSync(safePath, "utf-8");
17
+ }
18
+ catch {
19
+ throw new Error("File not readable as text");
20
+ }
21
+ const lines = content.split(/\r?\n/).length;
22
+ if (lines > MAX_LINES) {
23
+ throw new Error("Too many lines!");
24
+ }
25
+ return content;
26
+ }
@@ -0,0 +1,46 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { resolveSafePath } from "../utils/pathGuard.js";
4
+ import { listFile } from "./listFile.js";
5
+ import { flattenFileTree } from "../utils/flattenFIleTrees.js";
6
+ function isEmpty(str) {
7
+ return !str || str.trim().length === 0;
8
+ }
9
+ export function searchCode(query, dir) {
10
+ if (isEmpty(query)) {
11
+ throw new Error("The query is empty");
12
+ }
13
+ const rootDir = resolveSafePath(dir ?? ".");
14
+ const stats = fs.statSync(rootDir);
15
+ if (!stats.isDirectory()) {
16
+ throw new Error("The given path is not a directory");
17
+ }
18
+ const tree = listFile(rootDir);
19
+ const files = flattenFileTree(tree, rootDir);
20
+ const results = [];
21
+ const q = query.trim().toLowerCase();
22
+ for (const filePath of files) {
23
+ const ext = path.extname(filePath);
24
+ if ([".png", ".jpg", ".jpeg", ".zip", ".exe", ".pdf"].includes(ext)) {
25
+ continue;
26
+ }
27
+ let content;
28
+ try {
29
+ content = fs.readFileSync(filePath, "utf-8");
30
+ }
31
+ catch {
32
+ continue;
33
+ }
34
+ const lines = content.split("\n");
35
+ for (let i = 0; i < lines.length; i++) {
36
+ if (lines[i].toLowerCase().includes(q)) {
37
+ results.push({
38
+ filePath,
39
+ lineNumber: i + 1,
40
+ preview: lines[i].trim(),
41
+ });
42
+ }
43
+ }
44
+ }
45
+ return results;
46
+ }
@@ -0,0 +1,61 @@
1
+ import figlet from "figlet";
2
+ import chalk from "chalk";
3
+ const divider = chalk.gray("-".repeat(50));
4
+ export function renderLogo() {
5
+ const text = figlet.textSync("CodeFinder", {
6
+ font: "Standard",
7
+ horizontalLayout: "default",
8
+ verticalLayout: "default",
9
+ });
10
+ console.log("\n" + chalk.magenta(text));
11
+ console.log(chalk.whiteBright("by TWAHaa"));
12
+ console.log(divider);
13
+ }
14
+ export function renderHeader(title, subtitle) {
15
+ console.log("\n" + divider);
16
+ console.log(chalk.bold.white(title));
17
+ if (subtitle)
18
+ console.log(chalk.gray(subtitle));
19
+ console.log(divider);
20
+ }
21
+ export function renderTools(tools) {
22
+ if (!tools.length) {
23
+ console.log(chalk.yellow("No tools available."));
24
+ console.log(divider);
25
+ return;
26
+ }
27
+ console.log(chalk.cyan("Available tools:"));
28
+ for (const tool of tools) {
29
+ const desc = tool.description ? ` — ${tool.description}` : "";
30
+ console.log(`${chalk.green("•")} ${chalk.white(tool.name)}${chalk.gray(desc)}`);
31
+ }
32
+ console.log(divider);
33
+ }
34
+ export function logToolCall(name, args) {
35
+ console.log(divider);
36
+ console.log(`${chalk.blue("Tool call")} ${chalk.gray("→")} ${chalk.white(name)}`);
37
+ console.log(formatJSON(args));
38
+ console.log(divider);
39
+ }
40
+ export function logToolResult(result) {
41
+ console.log(chalk.blue("Tool result:"));
42
+ console.log(formatJSON(result));
43
+ console.log(divider);
44
+ }
45
+ export function logModelText(text) {
46
+ console.log(chalk.magenta("Gemini:"));
47
+ console.log(chalk.white(text.trim() || "(no text)"));
48
+ console.log(divider);
49
+ }
50
+ export function logError(err) {
51
+ console.error(chalk.red("Error:"), err);
52
+ console.log(divider);
53
+ }
54
+ function formatJSON(data) {
55
+ try {
56
+ return chalk.gray(JSON.stringify(data, null, 2));
57
+ }
58
+ catch {
59
+ return String(data);
60
+ }
61
+ }
@@ -0,0 +1,13 @@
1
+ export function flattenFileTree(nodes, rootPath) {
2
+ let files = [];
3
+ for (const node of nodes) {
4
+ const fullPath = `${rootPath}/${node.name}`;
5
+ if (node.type === "file") {
6
+ files.push(fullPath);
7
+ }
8
+ else if (node.children) {
9
+ files.push(...flattenFileTree(node.children, fullPath));
10
+ }
11
+ }
12
+ return files;
13
+ }
@@ -0,0 +1,18 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ const PROJECT_ROOT = process.cwd();
4
+ export function resolveSafePath(userPath) {
5
+ userPath = userPath.trim();
6
+ const candidatePath = path.resolve(PROJECT_ROOT, userPath);
7
+ let realCandidatePath;
8
+ try {
9
+ realCandidatePath = fs.realpathSync(candidatePath);
10
+ }
11
+ catch {
12
+ throw new Error("Path does not exist or cannot be accessed");
13
+ }
14
+ if (realCandidatePath !== PROJECT_ROOT && !realCandidatePath.startsWith(PROJECT_ROOT + path.sep)) {
15
+ throw new Error("Path escapes project root");
16
+ }
17
+ return realCandidatePath;
18
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "dependencies": {
3
+ "@clack/prompts": "^0.7.0",
4
+ "@google/generative-ai": "^0.24.1",
5
+ "@modelcontextprotocol/sdk": "^1.25.1",
6
+ "dotenv": "^17.2.3",
7
+ "chalk": "^5.3.0",
8
+ "figlet": "^1.9.4",
9
+ "zod": "^3.25.76"
10
+ },
11
+ "name": "@twahaa/codefinder",
12
+ "version": "1.0.0",
13
+ "description": "MCP server and interactive Gemini client for exploring and searching codebases.",
14
+ "main": "build/server.js",
15
+ "exports": {
16
+ ".": "./build/server.js",
17
+ "./client": "./build/client.js"
18
+ },
19
+ "type": "module",
20
+ "devDependencies": {
21
+ "@modelcontextprotocol/inspector": "^0.18.0",
22
+ "@types/node": "^25.0.3",
23
+ "@types/figlet": "^1.5.8",
24
+ "tsx": "^4.21.0",
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "scripts": {
28
+ "clean": "rm -rf build",
29
+ "build": "tsc",
30
+ "prepare": "npm run build",
31
+ "client:dev": "tsx src/client.ts",
32
+ "search": "tsx src/cli/search.ts",
33
+ "server:test": "echo \"Error: no test specified\" && exit 1",
34
+ "server:build": "tsc",
35
+ "server:build:watch": "tsc --watch",
36
+ "server:dev": "tsx src/server.ts",
37
+ "server:inspect": "DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector npm run server:dev"
38
+ },
39
+ "bin": {
40
+ "codefinder": "build/client.js"
41
+ },
42
+ "files": [
43
+ "build",
44
+ "README.md",
45
+ "LICENSE"
46
+ ],
47
+ "keywords": [],
48
+ "author": "",
49
+ "license": "ISC"
50
+ }