Hey folks, today I want to show you how to create an MCP server with special assistants: Kiro and Sonnet Claude. ✨
But what is this new term? MCP. Is it a buzzword? I don't know, but recently it is has been very discussed recently.
So, let's dive into the MCP concept.
MCP: a new protocol by Anthropic
I'll start by showing right away the schema of the MCP Protocol:
Simple: with this protocol, we can connect our AI applications to the world.
We can imagine this protocol like a USB port to connect the LLM to other services, like Gmail, Notion, Obsidian, Drive or more simply a DB or API service, with a unique standard. This is the work done by Anthropic.
Now let's zoom in on this diagram 🔭
As you can see, the MCP is a layer that simplifies the communication between LLM and another service.
We define a real scenario.
In this last year, I've focused on my diet and gym results. So I want to know what I eat and the nutrients in food.
There are a ton of mobile applications to search for foods and get more info about them, but I'd like to search with more flexibility using natural language, for example, in a specific dataset when I'm using an AI client like Claude. The MCP protocol is the right way. We can ask, for example:
"I ate two eggs Bio Carrefour", and I'll expect a response with nutritional information.
Great, but how?
Let's move on to practice.
Use Vibe coding to create our MCP Server
As the literature says, we have to use the code vibe formula
$Vibe = (Project Soul + Your Intuition + Model's Character)$
If you are curious, read this book by David Gillette.
The first argument is What and Why of our project.
The second is our unique contribution as a human creators. It's our gut feeling, our creative spark, our accumulated experience.
The third is about understanding the specific AI model we are working with.
With this information, we can use Kiro's agent to address it to create our MCP Server.
Here is an example of text:
1) Project Soul — What & Why
Goal: build an MCP Server that communicates with the dataset and APIs of Open Food Facts. The server should receive requests from an MCP Client and fetch food-related information directly from the official API (ingredients, nutrition facts, allergens, categories, brands), never generating or hallucinating data.
Why: this enables AI systems to integrate verifiable, real-world food data in a reliable and traceable way.
Expected outcome: an MCP implementation with well-defined tools that can search, filter, and retrieve products, always returning source: "openfoodfacts:<endpoint>".
2) Your Intuition — Human Spark
Keep the architecture clean and modular: separate the MCP protocol layer from the connectors to Open Food Facts.
Define strict JSON Schemas for inputs and outputs of all tools so Claude Sonnet can safely orchestrate the calls.
Add support for language localisation (lang), since Open Food Facts is multilingual.
Implement lightweight caching to avoid redundant API requests.
Provide clear error messages, always including a hint field to guide the client in correcting invalid inputs.
Code style requirement:
Do not use class constructors.
Use const declarations and arrow functions for all functions, exports, and callbacks.
Prefer functional composition over OOP patterns.
Use Typescript for entire project.
3) Model’s Character — Who You Are
Model to use: Claude Sonnet.
Character traits:
excels at producing structured, logical, and safe outputs;
prefers explicit schemas and typed definitions;
skilled at summarising without losing precision;
acts as a connector to the API, not as a data source.
Constraint: every output must reference the Open Food Facts source (source).
We can use this prompt on Kiro, and then we will have our MCP Server that gets the information from Open Food Facts via API (in this case, not authenticated)
Client MCP
Now we need the client MCP to communicate with the Server.
We have to create a .kiro
folder in our project and inside define a file called mcp.json
with this content
{
"mcpServers": {
"calormeal": {
"command": "node",
"args": ["/Users/myUser/projects/new-calormeal/build/index.js"],
"disabled": false,
"autoApprove": ["search_products", "get_product", "search_by_nutrition", "search_by_category"]
}
}
}
That's all.
Let's try
Now we can ask our server something, like this
I ate 2 organic eggs from Carrefour. How much protein did I consume?
This is the result
As you can see, the response does not come from Claude's model, but our server is called, which in turn calls the Open Food Facts API and returns the information according to the structure we have defined.
The code
Let's see the code generated
We have an openfoodfacts.ts
file that defines a direct interface with the openfoodfacts API.
We have for example, the searchProduct
function.
export const searchProducts = async (input: SearchProductsInput): Promise<ProductListResponse | ErrorResponse> => {
const cacheKey = `search:${JSON.stringify(input)}`;
const cached = getCached<ProductListResponse>(cacheKey);
if (cached) return cached;
try {
const params = new URLSearchParams({
search_terms: input.query,
page: input.page?.toString() || '1',
page_size: input.page_size?.toString() || '20',
sort_by: input.sort_by || 'popularity',
json: '1'
});
const url = `https://${input.lang || 'world'}.openfoodfacts.org/cgi/search.pl?${params}`;
const response = await fetch(url);
if (!response.ok) {
return createError(
`API request failed with status ${response.status}`,
'Check your network connection and try again with a simpler query'
);
}
const data = await response.json() as any;
const result: ProductListResponse = {
products: (data.products || []).map(normalizeProduct),
count: data.count || 0,
page: input.page || 1,
page_size: input.page_size || 20,
source: 'openfoodfacts'
};
setCache(cacheKey, result);
return result;
} catch (error) {
return createError(
`Network error: ${error instanceof Error ? error.message : 'Unknown error'}`,
'Verify your internet connection and ensure the query contains valid characters'
);
}
};
The server.ts
defines an MCP server, and there are 4 registered tools:
- search_products - General search by name/brand
- get_product - Product details via barcode
- search_by_nutrition - Filter by nutritional values
- search_by_category - Search by food category
and then
- ListToolsRequest - Returns a list of available tools
- CallToolRequest - Runs the requested tool with input validation
Here we define the Server
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
const server = new Server(
{
name: 'openfoodfacts-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
and then we define the tools for each action like this:
const tools = [
{
name: 'search_products',
description: 'Search for food products by name, brand, or keywords in Open Food Facts database',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query (product name, brand, keywords)',
minLength: 1
},
lang: {
type: 'string',
description: 'Language code (en, fr, es, de, etc.)',
default: 'en'
},
page: {
type: 'number',
description: 'Page number for pagination',
minimum: 1,
default: 1
},
page_size: {
type: 'number',
description: 'Number of results per page',
minimum: 1,
maximum: 100,
default: 20
},
sort_by: {
type: 'string',
enum: ['product_name', 'popularity', 'created_t'],
description: 'Sort results by field',
default: 'popularity'
}
},
required: ['query']
}
},
/* [other tools] */
Here, the setRequestHandler associates a handler function with a specific request type:
- Pattern matching – When a request matches the schema, the function is executed
- Two registered handlers:
ListToolsRequestSchema → returns the list of available tools
CallToolRequestSchema → executes a specific tool
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'search_products': {
const input = SearchProductsInputSchema.parse(args);
const result = await searchProducts(input);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
...
/* [other cases] */
and finally run the server
export const runServer = async (): Promise<void> => {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Open Food Facts MCP Server running on stdio');
};
Use Claude Desktop
Also, you can use the Claude desktop. Just configure this file claude_desktop_config.json
Then run the application, and under settings, you can find the MCP
and the ours functions
The result is
You can find all the code on my repo.
Remeber
You can find a lot of servers here (official and unofficial), but if you want to create one from zero to integrate your private service or for another reason, you can follow the previous steps for a good starter.
Thanks for the reading, and see you soon 👋🏻.
Top comments (0)