Empowering AI: The MCP Guide to Tool Discovery and Dynamic Integration
You successfully built your first readFile tool in a previous guideβa fantastic achievement! But imagine stepping into a new restaurant where no menu is presented. How would you ever know what delicious options await? This very challenge is precisely what the Model Context Protocol (MCP) discovery system resolves. In this article, weβll construct the foundational "menu" that enables AI systems to automatically uncover and intelligently utilize your custom tools.
Introduction: The Genius of Auto-Discovery
Having spent over 15 years immersed in API development, I've encountered numerous integration patterns. Yet, MCP introduces a truly elegant solution: auto-discovery. Instead of hard-coding every specific integration, an AI can dynamically query your server to understand its capabilities. It's like an API that can confidently introduce itself and its offerings.
This paradigm shift is transformative. Gone are the days of crafting distinct connectors for each AI platform. With MCP, you establish a universal standard that all compatible AI agents can interpret. Master this robust system, and you'll effortlessly expose a multitude of tools without ever needing to alter the AI's core programming.
Decoding the AI-Server Dialogue: A Request's Journey
Before diving into the implementation details, let's visualize the complete lifecycle of an interaction between an AI and your MCP server. Grasping this flow is vital for understanding where the discovery mechanism fits in.
Phase 1: Dynamic Discovery (Our Focus Today)
You: "Claude, could you list the contents of my projects directory?"
Claude: "I haven't yet learned about this server's tools. Let me explore its capabilities..."
Claude β Server: GET /mcp/tools
Server β Claude: Here's a comprehensive list of all my available tools and their descriptions.
Claude: "Excellent! A listFiles tool exists. That's precisely what I need for this request."
Phase 2: User Validation (Managed by the AI Application)
Claude Application β You: "Do you permit the listFiles tool to access the /projects folder?"
You: "Yes, proceed."
Phase 3: Action Execution
Claude β Server: POST /mcp/execute with {"tool": "listFiles", "params": {"path": "/projects"}}
Server β Claude: The outcome of the tool's execution.
Claude β You: "Here are the items found in your projects folder: ..."
Our deep dive today focuses exclusively on Phase 1: the discovery process.
The Standard Blueprint for MCP Tool Discovery
The MCP protocol defines a consistent JSON structure for articulating your tools' functionalities. Hereβs a typical example of what an AI expects to receive:
{
"protocol_version": "1.0",
"server_info": {
"name": "My MCP Server",
"version": "1.0.0",
"description": "MCP server for local file access"
},
"tools": [
{
"name": "readFile",
"description": "Reads the content of a text file",
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to the file to read"
}
},
"required": ["file_path"]
}
}
]
}
This schema encapsulates three essential pieces of data:
-
protocol_version: Specifies the MCP version being implemented. -
server_info: Provides metadata about your server instance. -
tools: Contains a full listing of your available tools, each with its detailed input schema.
Notably, the input_schema leverages the well-established JSON Schema standard. Think of it as self-generating documentation that an AI can natively parse and comprehend.
Laying the Groundwork: The Protocol Manager
Letβs begin by structuring our MCP protocol implementation. Create the file src/mcp/protocol.ts:
// src/mcp/protocol.ts
/**
* The implemented MCP protocol version.
*/
export const MCP_PROTOCOL_VERSION = "1.0";
/**
* Structure for server metadata.
*/
export interface ServerInfo {
name: string;
version: string;
description: string;
author?: string;
capabilities?: string[];
}
/**
* Defines the input schema for a tool, adhering to JSON Schema standards.
*/
export interface InputSchema {
type: "object";
properties: {
[paramName: string]: {
type: string;
description: string;
enum?: string[];
default?: any;
};
};
required: string[];
}
/**
* Comprehensive description of an MCP tool.
*/
export interface ToolDescription {
name: string;
description: string;
input_schema: InputSchema;
}
/**
* The complete response structure for tool discovery.
*/
export interface DiscoveryResponse {
protocol_version: string;
server_info: ServerInfo;
tools: ToolDescription[];
}
/**
* Our server's descriptive information.
*/
export const SERVER_INFO: ServerInfo = {
name: "MCP File Server",
version: "1.0.0",
description: "An MCP server designed for local file system management.",
author: "Nicolas Dabène",
capabilities: [
"file_reading",
"directory_listing",
"file_search"
]
};
These TypeScript interfaces provide a robust foundation. Every tool we define will subsequently adhere to the ToolDescription format.
Adapting Tools to the MCP Standard
Previously, our tools had basic definitions. Now, we'll enhance them to conform to the comprehensive MCP format. Update src/tools/readFile.ts:
// src/tools/readFile.ts
import fs from 'fs/promises';
import path from 'path';
import { ToolResponse } from '../types/mcp';
import { ToolDescription, InputSchema } from '../mcp/protocol';
export interface ReadFileParams {
file_path: string;
encoding?: 'utf-8' | 'ascii' | 'base64';
}
export async function readFile(params: ReadFileParams): Promise<ToolResponse> {
try {
if (!params.file_path) {
return {
success: false,
error: "The 'file_path' parameter is required"
};
}
const absolutePath = path.resolve(params.file_path);
try {
await fs.access(absolutePath);
} catch {
return {
success: false,
error: `File not found: ${params.file_path}`
};
}
const stats = await fs.stat(absolutePath);
if (!stats.isFile()) {
return {
success: false,
error: "The specified path is not a file"
};
}
const MAX_FILE_SIZE = 10 * 1024 * 1024;
if (stats.size > MAX_FILE_SIZE) {
return {
success: false,
error: `File too large (max ${MAX_FILE_SIZE / 1024 / 1024} MB)`
};
}
const encoding = params.encoding || 'utf-8';
const content = await fs.readFile(absolutePath, encoding);
return {
success: true,
content: content,
metadata: {
path: absolutePath,
size: stats.size,
encoding: encoding,
lastModified: stats.mtime
}
};
} catch (error: any) {
return {
success: false,
error: `Error reading file: ${error.message}`
};
}
}
/**
* Input schema for readFile in JSON Schema format.
*/
const readFileInputSchema: InputSchema = {
type: "object",
properties: {
file_path: {
type: "string",
description: "Absolute or relative path to the file to read"
},
encoding: {
type: "string",
description: "File encoding (e.g., UTF-8, ASCII, Base64)",
enum: ["utf-8", "ascii", "base64"],
default: "utf-8"
}
},
required: ["file_path"]
};
/**
* The complete MCP description for the readFile tool.
*/
export const readFileDescription: ToolDescription = {
name: "readFile",
description: "Reads the content of a text file from the local file system, supporting UTF-8, ASCII, and Base64 encodings.",
input_schema: readFileInputSchema
};
Notice the significant enhancements:
- A formal JSON Schema: This precisely outlines the expected parameters for the tool.
- Enriched Metadata: Includes valuable details like default values and enumerations.
- A Detailed Description: Offers clear guidance to the AI on when and how to invoke this specific tool.
Letβs apply the same transformation to our listFiles tool. Update src/tools/listFiles.ts:
// src/tools/listFiles.ts
import fs from 'fs/promises';
import path from 'path';
import { ToolResponse } from '../types/mcp';
import { ToolDescription, InputSchema } from '../mcp/protocol';
export interface ListFilesParams {
directory_path: string;
include_hidden?: boolean;
recursive?: boolean;
}
export async function listFiles(params: ListFilesParams): Promise<ToolResponse> {
try {
if (!params.directory_path) {
return {
success: false,
error: "The 'directory_path' parameter is required"
};
}
const absolutePath = path.resolve(params.directory_path);
const stats = await fs.stat(absolutePath);
if (!stats.isDirectory()) {
return {
success: false,
error: "The specified path is not a directory"
};
}
// Read directory content
let files = await fs.readdir(absolutePath);
// Filter hidden files if necessary
if (!params.include_hidden) {
files = files.filter(file => !file.startsWith('.'));
}
// Get details for each file
const filesWithDetails = await Promise.all(
files.map(async (file) => {
const filePath = path.join(absolutePath, file);
const fileStats = await fs.stat(filePath);
return {
name: file,
type: fileStats.isDirectory() ? 'directory' : 'file',
size: fileStats.size,
lastModified: fileStats.mtime,
permissions: fileStats.mode
};
})
);
return {
success: true,
content: JSON.stringify(filesWithDetails, null, 2),
metadata: {
path: absolutePath,
count: filesWithDetails.length,
include_hidden: params.include_hidden || false
}
};
} catch (error: any) {
return {
success: false,
error: `Error reading directory: ${error.message}`
};
}
}
const listFilesInputSchema: InputSchema = {
type: "object",
properties: {
directory_path: {
type: "string",
description: "Absolute or relative path to the directory to list"
},
include_hidden: {
type: "boolean",
description: "Flag to include hidden files (starting with '.')",
default: false
},
recursive: {
type: "boolean",
description: "Flag to list subdirectories recursively (not yet implemented)",
default: false
}
},
required: ["directory_path"]
};
export const listFilesDescription: ToolDescription = {
name: "listFiles",
description: "Lists files and subdirectories within a specified directory. Options include filtering hidden files and (future) recursive listing.",
input_schema: listFilesInputSchema
};
Building the Centralized Tool Registry
Next, we'll establish a central registry to consolidate all our custom tools. Create src/mcp/registry.ts:
// src/mcp/registry.ts
import { ToolDescription } from './protocol';
import { ToolResponse } from '../types/mcp';
import { readFile, readFileDescription } from '../tools/readFile';
import { listFiles, listFilesDescription } from '../tools/listFiles';
/**
* Type definition for a tool's executable function.
*/
type ToolFunction = (params: any) => Promise<ToolResponse>;
/**
* Manages all available tools, their descriptions, and execution.
*/
class ToolRegistry {
private tools: Map<string, ToolFunction> = new Map();
private descriptions: Map<string, ToolDescription> = new Map();
/**
* Registers a new tool with its description and implementation.
*/
register(description: ToolDescription, implementation: ToolFunction) {
this.tools.set(description.name, implementation);
this.descriptions.set(description.name, description);
console.log(`β
Tool registered: ${description.name}`);
}
/**
* Retrieves all registered tool descriptions.
*/
getAllDescriptions(): ToolDescription[] {
return Array.from(this.descriptions.values());
}
/**
* Fetches the description for a specific tool by name.
*/
getDescription(toolName: string): ToolDescription | undefined {
return this.descriptions.get(toolName);
}
/**
* Executes a registered tool with provided parameters.
*/
async execute(toolName: string, params: any): Promise<ToolResponse> {
const tool = this.tools.get(toolName);
if (!tool) {
return {
success: false,
error: `Tool '${toolName}' not found. Available tools: ${Array.from(this.tools.keys()).join(', ')}`
};
}
try {
return await tool(params);
} catch (error: any) {
return {
success: false,
error: `Error executing '${toolName}': ${error.message}`
};
}
}
/**
* Checks if a tool with the given name exists.
*/
has(toolName: string): boolean {
return this.tools.has(toolName);
}
/**
* Returns the count of currently registered tools.
*/
count(): number {
return this.tools.size;
}
}
// Export a singleton instance of the ToolRegistry.
export const toolRegistry = new ToolRegistry();
// Register all our tools upon application startup.
toolRegistry.register(readFileDescription, readFile);
toolRegistry.register(listFilesDescription, listFiles);
This ToolRegistry serves as the central nervous system of our MCP system. It's responsible for:
- Maintaining an up-to-date list of all exposed tools.
- Facilitating consistent tool execution.
- Providing structured descriptions crucial for AI discovery.
- Centralizing error handling for tool operations.
Implementing MCP Endpoints with Express
Now, let's configure our Express server to expose the standard MCP endpoints. Replace the content of src/index.ts:
// src/index.ts
import express, { Request, Response } from 'express';
import { MCP_PROTOCOL_VERSION, SERVER_INFO, DiscoveryResponse } from './mcp/protocol';
import { toolRegistry } from './mcp/registry';
const app = express();
const PORT = 3000;
// Essential middleware for JSON body parsing.
app.use(express.json());
// Request logging middleware for visibility.
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
next();
});
// ============================================
// STANDARD MCP ROUTES
// ============================================
/**
* Root endpoint: Provides basic server information.
*/
app.get('/', (req: Request, res: Response) => {
res.json({
message: 'MCP File Server Operational',
version: SERVER_INFO.version,
protocol_version: MCP_PROTOCOL_VERSION,
status: 'online',
endpoints: {
discovery: '/mcp/tools',
execute: '/mcp/execute',
health: '/health'
}
});
});
/**
* Discovery endpoint: The AI's comprehensive "tool menu".
* This is the primary route for AI agents to learn about available tools.
*/
app.get('/mcp/tools', (req: Request, res: Response) => {
const response: DiscoveryResponse = {
protocol_version: MCP_PROTOCOL_VERSION,
server_info: SERVER_INFO,
tools: toolRegistry.getAllDescriptions()
};
console.log(`π Discovery requested - Exposing ${toolRegistry.count()} tools.`);
res.json(response);
});
/**
* Unified execution endpoint: Handles all tool invocations.
* Expected format: POST /mcp/execute with body: { "tool": "toolName", "params": {...} }
*/
app.post('/mcp/execute', async (req: Request, res: Response) => {
const { tool, params } = req.body;
// Basic request validation.
if (!tool) {
return res.status(400).json({
success: false,
error: "The 'tool' parameter is mandatory for execution."
});
}
console.log(`βοΈ Execution initiated for: ${tool}`);
// Delegate execution to the centralized registry.
const result = await toolRegistry.execute(tool, params || {});
// Log execution outcome.
if (result.success) {
console.log(`β
Execution successful for: ${tool}`);
} else {
console.log(`β Execution failed for: ${tool} - Reason: ${result.error}`);
}
res.json(result);
});
/**
* Endpoint for retrieving a specific tool's description.
*/
app.get('/mcp/tools/:toolName', (req: Request, res: Response) => {
const { toolName } = req.params;
const description = toolRegistry.getDescription(toolName);
if (!description) {
return res.status(404).json({
success: false,
error: `Tool '${toolName}' not found.`
});
}
res.json(description);
});
/**
* Health check endpoint: Reports server status.
*/
app.get('/health', (req: Request, res: Response) => {
res.json({
status: 'healthy',
uptime: process.uptime(),
registered_tools_count: toolRegistry.count(),
timestamp: new Date().toISOString()
});
});
// ============================================
// BACKWARD COMPATIBILITY (optional but useful for migration)
// ============================================
/**
* Deprecated direct tool endpoint, kept for older tests or clients.
* @deprecated Use /mcp/execute for unified tool execution.
*/
app.post('/tools/:toolName', async (req: Request, res: Response) => {
const { toolName } = req.params;
const params = req.body;
console.log(`β οΈ Direct tool endpoint '/tools/${toolName}' accessed (deprecated).`);
const result = await toolRegistry.execute(toolName, params);
res.json(result);
});
// ============================================
// SERVER STARTUP ROUTINE
// ============================================
app.listen(PORT, () => {
console.log('βββββββββββββββββββββββββββββββββββββββ');
console.log('π MCP File Server is now Live!');
console.log('βββββββββββββββββββββββββββββββββββββββ');
console.log(`π Server URL: http://localhost:${PORT}`);
console.log(`π Tool Discovery: http://localhost:${PORT}/mcp/tools`);
console.log(`βοΈ Tool Execution: POST http://localhost:${PORT}/mcp/execute`);
console.log(`π§ Active Tools: ${toolRegistry.count()}`);
console.log('βββββββββββββββββββββββββββββββββββββββ');
});
Our Express application now fully supports the MCP protocol, featuring three core endpoints:
-
GET /mcp/tools: For comprehensive tool discovery. -
POST /mcp/execute: For unified invocation of any registered tool. -
GET /mcp/tools/:toolName: For retrieving specific details about a single tool.
Verifying the Discovery System in Action
Let's restart our server to witness the full system in operation:
npm run dev
You should observe output similar to this:
βββββββββββββββββββββββββββββββββββββββ
π MCP File Server is now Live!
βββββββββββββββββββββββββββββββββββββββ
π Server URL: http://localhost:3000
π Tool Discovery: http://localhost:3000/mcp/tools
βοΈ Tool Execution: POST http://localhost:3000/mcp/execute
π§ Active Tools: 2
βββββββββββββββββββββββββββββββββββββββ
β
Tool registered: readFile
β
Tool registered: listFiles
Test 1: Full Tool Discovery
Execute the following curl command to perform a complete discovery:
curl http://localhost:3000/mcp/tools | json_pp
Success! Your AI can now effortlessly discover all your tools, complete with their exhaustive descriptions.
Test 2: Execution via the Unified Endpoint
Now, let's test executing a tool through the consolidated endpoint:
curl -X POST http://localhost:3000/mcp/execute \
-H "Content-Type: application/json" \
-d '{
"tool": "readFile",
"params": {
"file_path": "test.txt"
}
}'
The Architectural Implications: A Game Changer for AI
This robust discovery system fundamentally reshapes how AI integrations are designed and implemented.
Pre-MCP Era: The Challenge of Rigid Integrations
Historically, each AI agent required explicit, tool-specific programming:
// AI code designed for specific tool integrations
if (userWantsToReadFile) {
callReadFileAPI(userParams);
} else if (userWantsToListFiles) {
callListFilesAPI(userParams);
}
This approach led to brittle, complex, and difficult-to-scale integrations.
Post-MCP Era: The Power of Auto-Discovery
With MCP, AI gains the ability to dynamically assess and leverage capabilities:
// AI dynamically discovers and utilizes available tools
const tools = await discoverTools();
const tool = tools.find(t => matchesUserRequest(t)); // AI logic to select the best tool
await executeTool(tool.name, userParams);
This represents a monumental leap towards more flexible, intelligent, and autonomous AI systems.
Conclusion: Embracing a New Era of AI Integration
The MCP tool discovery system heralds a transformative shift in AI integration methodologies. Rather than developing bespoke connectors for every scenario, you now construct standardized, modular "building blocks" that any MCP-compliant AI can dynamically identify and assemble to fulfill user requests.
In this guide, we've achieved several crucial milestones:
- Implemented the full MCP protocol, encompassing both discovery and execution.
- Established a centralized registry for managing and exposing tools.
- Structured our tools with the clarity and rigor of JSON Schema.
- Validated the entire system using standard HTTP clients.
- Simulated the complete AI-server interaction flow.
What's next? The thrilling prospect of connecting your newly built MCP server to a live AI platform, such as Claude Desktop. Once integrated, you'll witness the magic unfold as your AI autonomously reads files, navigates directories, and leverages all the custom tools you've exposed.
MCP is more than just a technical specification; it's a fresh perspective on the synergy between humans and AI. Your bespoke tools seamlessly become natural extensions of AI capabilities, eliminating traditional technical friction.
Article originally published on November 12, 2025, by Nicolas DabΓ¨ne β a seasoned PHP & PrestaShop Expert with over 15 years of experience in software architecture and AI integration.
Continue Your MCP Journey:
- Understanding the Model Context Protocol (MCP): A Simple Conversation
- Create Your First MCP Server: TypeScript Project Setup
- Create Your First MCP Tool: The readFile Tool Explained
- Secure Your MCP Server: Permissions, Validation and Protection
- Connect Your MCP Server to Claude Desktop: The Complete Integration
Enjoyed this deep dive into AI tool discovery? For more cutting-edge insights into software architecture, AI integration, and development tips, make sure to subscribe to my YouTube channel at youtube.com/@ndabene06 and connect with me on LinkedIn at linkedin.com/in/nicolas-dabène-473a43b8! Let's build smarter systems together!
Top comments (0)