still cooking_
4 min readopen-aeo

OpenAEO Dev Log #3: Connecting to Claude via the Model Context Protocol (MCP)

OpenAEO Dev Log #3: Connecting to Claude via the Model Context Protocol (MCP)

OpenAEO Dev Log #3: Connecting to Claude via the Model Context Protocol (MCP)

In my previous logs, I designed the core domain logic (the "Brain") and the infrastructure adapters (the "Hands") of OpenAEO. I used Hexagonal Architecture to ensure my code was completely isolated from external dependencies.

Today, it was time to build the "Mouth" and "Ears"—the Presentation Layer. For an AI tool, that means implementing the Model Context Protocol (MCP). This is the final stage that turns a silent, local script into an interactive tool inside Claude Desktop.

Here is how I wired the final pieces together, the architectural hurdles I overcame, and how I finally brought the system online.

Stage 4: The Presentation Layer (MCP)

MCP acts as a universal USB port for AI models. It allows Claude to discover my local tools, understand what parameters they need, and execute them over standard input/output (stdio).

I broke this into two distinct files to keep the routing separate from the execution:

1. The Tool Execution (src/mcp/tools.ts)

This file acts as the conductor. I created functions like handleAeoCheck and handleAeoReport. They take the external dependencies (the Perplexity and Storage adapters) and the user input, pass them into my pure parseAeoResponse logic, and format the output into a clean string for Claude to digest.

Architect's Note: For the batch reporting tool, I intentionally engineered a 2-second setTimeout delay between queries. When building automated tools, you must respect the rate limits of your upstream providers, or you will get IP-banned before you even launch.

2. The Server Routing (src/mcp/server.ts)

This is where the actual MCP SDK lives. I defined a ListToolsRequestSchema that acts as the instruction manual for Claude.

The Lesson: LLMs require extreme precision. If you define a parameter as targetDomain, you must write a crystal-clear description (e.g., "The domain to look for, like 'notion.so'"). If the description is vague, the AI will hallucinate the wrong inputs.

I also learned a hard lesson about MCP servers: You cannot use console.log. Because the server communicates with Claude over stdout, a stray console.log("Debugging...") will corrupt the JSON stream and crash the AI. All debugging must go through console.error.

Stage 5: The Entry Point

With all the components built, I needed a single file to wire them together. In Hexagonal Architecture, the entry point is the only place that knows about everything.

File: src/index.ts I used an immediately invoked async function to handle top-level await. Here, I applied the Fail Fast principle. The very first thing the app does is check for process.env.PERPLEXITY_API_KEY. If it's missing, it throws a fatal error and exits (process.exit(1)). You never want a server to boot up in a broken state and fail silently later.

The TypeScript Strict Mode Crucible

Before deploying, I ran pnpm build to compile the TypeScript to JavaScript. My strict tsconfig.json immediately caught two things:

  1. ESM Module Resolution: Because I was using "moduleResolution": "NodeNext", Node.js requires explicit .js extensions in local imports—even when importing .ts files.
  2. Implicit any: My .map() functions were missing explicit types, which is a massive liability in production.

I fixed the imports, strongly typed my parameters, and got a clean, silent build.

Stage 6: The Client Integration

The final step was plugging my compiled dist/index.js file into Claude Desktop.

I edited my local claude_desktop_config.json file to register the open-aeo server. Since I already had some existing preferences in that file, I had to carefully merge the mcpServers object at the root level to avoid wiping out my other settings.

I passed in the absolute path to my compiled project and injected my Perplexity API key as an environment variable.

The Result

I restarted Claude, opened a chat, and typed: "Can you use the aeo_check tool to see if the domain 'notion.so' is cited for the query 'best note taking apps'?"

The response was instant. Claude recognized the intent, executed my local code, fetched the live web data, ran my regex parsing, saved the history locally, and summarized the exact position Notion held in the AI search results.

Looking Forward

OpenAEO is now a fully functional, self-hosted local tool. By strictly adhering to Ports and Adapters, this exact same codebase is ready to be scaled. When I want to turn this into a multi-tenant SaaS, I simply write a PostgresAdapter and swap it into index.ts. The core logic won't change by a single line.