index.tsā¢8.48 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import * as fs from "fs/promises";
import * as path from "path";
import * as os from "os";
// Default context directory
const CONTEXT_DIR = path.join(os.homedir(), ".mycontext");
interface ProjectStructure {
name: string;
type: "file" | "directory";
path: string;
children?: ProjectStructure[];
}
// Ensure context directory exists
async function ensureContextDir() {
try {
await fs.access(CONTEXT_DIR);
} catch {
await fs.mkdir(CONTEXT_DIR, { recursive: true });
}
}
// List all projects (top-level directories)
async function listProjects(): Promise<string[]> {
await ensureContextDir();
const entries = await fs.readdir(CONTEXT_DIR, { withFileTypes: true });
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
}
// Get project structure recursively
async function getProjectStructure(
projectName: string,
subPath: string = ""
): Promise<ProjectStructure> {
const fullPath = path.join(CONTEXT_DIR, projectName, subPath);
const stats = await fs.stat(fullPath);
const name = subPath ? path.basename(subPath) : projectName;
if (stats.isFile()) {
return {
name,
type: "file",
path: subPath || projectName,
};
}
const entries = await fs.readdir(fullPath, { withFileTypes: true });
const children = await Promise.all(
entries.map((entry) =>
getProjectStructure(
projectName,
subPath ? path.join(subPath, entry.name) : entry.name
)
)
);
return {
name,
type: "directory",
path: subPath || projectName,
children,
};
}
// Read context file
async function readContext(
projectName: string,
filePath: string
): Promise<string> {
const fullPath = path.join(CONTEXT_DIR, projectName, filePath);
// Security check: ensure path is within context directory
const resolvedPath = path.resolve(fullPath);
const resolvedContextDir = path.resolve(CONTEXT_DIR);
if (!resolvedPath.startsWith(resolvedContextDir)) {
throw new Error("Invalid path: outside context directory");
}
return await fs.readFile(fullPath, "utf-8");
}
// Search across all markdown files
async function searchContext(query: string): Promise<any[]> {
const projects = await listProjects();
const results: any[] = [];
for (const project of projects) {
const matches = await searchInProject(project, "", query);
if (matches.length > 0) {
results.push({
project,
matches,
});
}
}
return results;
}
async function searchInProject(
projectName: string,
subPath: string,
query: string
): Promise<any[]> {
const fullPath = path.join(CONTEXT_DIR, projectName, subPath);
const entries = await fs.readdir(fullPath, { withFileTypes: true });
const matches: any[] = [];
for (const entry of entries) {
const entryPath = path.join(subPath, entry.name);
if (entry.isDirectory()) {
const subMatches = await searchInProject(projectName, entryPath, query);
matches.push(...subMatches);
} else if (entry.name.endsWith(".md")) {
const content = await readContext(projectName, entryPath);
const lines = content.split("\n");
const queryLower = query.toLowerCase();
lines.forEach((line, index) => {
if (line.toLowerCase().includes(queryLower)) {
matches.push({
file: entryPath,
line: index + 1,
content: line.trim(),
});
}
});
}
}
return matches;
}
// Create MCP server
const server = new Server(
{
name: "mycontext",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_projects",
description:
"List all available projects in the context directory (~/.mycontext/)",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "get_project_structure",
description:
"Get the complete directory structure of a specific project",
inputSchema: {
type: "object",
properties: {
project_name: {
type: "string",
description: "Name of the project",
},
},
required: ["project_name"],
},
},
{
name: "read_context",
description:
"Read the content of a specific context file within a project",
inputSchema: {
type: "object",
properties: {
project_name: {
type: "string",
description: "Name of the project",
},
file_path: {
type: "string",
description:
"Relative path to the file within the project (e.g., 'backend/gin.md')",
},
},
required: ["project_name", "file_path"],
},
},
{
name: "search_context",
description:
"Search for a keyword across all context files in all projects",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query",
},
},
required: ["query"],
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
if (!args) {
throw new Error("Arguments are required");
}
switch (name) {
case "list_projects": {
const projects = await listProjects();
return {
content: [
{
type: "text",
text: JSON.stringify(
{
projects,
context_directory: CONTEXT_DIR,
},
null,
2
),
},
],
};
}
case "get_project_structure": {
const projectName = args.project_name as string;
if (!projectName) {
throw new Error("project_name is required");
}
const structure = await getProjectStructure(projectName);
return {
content: [
{
type: "text",
text: JSON.stringify(structure, null, 2),
},
],
};
}
case "read_context": {
const projectName = args.project_name as string;
const filePath = args.file_path as string;
if (!projectName || !filePath) {
throw new Error("project_name and file_path are required");
}
const content = await readContext(projectName, filePath);
return {
content: [
{
type: "text",
text: content,
},
],
};
}
case "search_context": {
const query = args.query as string;
if (!query) {
throw new Error("query is required");
}
const results = await searchContext(query);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
query,
total_results: results.reduce(
(sum, r) => sum + r.matches.length,
0
),
results,
},
null,
2
),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MyContext MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});