Generative AI
Building robust backends for AI applications often involves integrating various components. The Model Context Protocol (MCP) offers a standardized communication layer, and Genkit provides a powerful framework to leverage it. This article guides you through creating a custom MCP server using Genkit, from defining data schemas to interacting with the server using development tools like Cline.
This article assumes some familiarity with Genkit concepts like flows and tools. Learn more about Genkit: https://firebase.google.com/docs/genkit
If you prefer this tutorial in video format, check out:
1. Defining Your Data Schema
Before building any application, you need to define the structure of your data. In this example, we're building a system to manage website orders for a consulting company. We use zod, a popular schema declaration library, integrated with Genkit.
The OrderSchema defines the necessary fields for each order:
const OrderSchema = z.object({
id: z.string().describe("Order Id. Format: A-XXX-XXX, ex. A-123-456"),
name: z.string().describe("Name of the business"),
colorScheme: z.object({
mainColor: z.string().describe("RGB color prefixed with #, ex. #FFAA66"),
highlight: z.string().describe("RGB color prefixed with #, ex. #AA6611"),
textColor: z.string().describe("RGB color prefixed with #, ex. #224455"),
}),
email: z.string(),
jobDescription: z
.string()
.describe(
"Detailed description of the website they need. Ex. if they are a construction " +
"company describe what they want on the home, contact age, testimonials, etc. " +
"pages. Not too crazy, usually static website."
),
address: z.string(),
});This schema clearly outlines each piece of information associated with an order, including nested details like the colorScheme.
2. Generating Realistic Test Data (optional)
During development, you need ways to both populate your system with test data and inspect that data. Manually creating test data can be tedious. Genkit integrates with tools like genthetic to automatically generate realistic (but fake) data based on your schema.
Here's how we define a Genkit flow (populateData) to generate 20 unique order entries and store them in Firestore:
// Adds fake orders to firestore
ai.defineFlow("populateData", async () => {
const OrderSynth = g
.defineType({
name: "Order",
schema: OrderSchema,
})
.generate({
unique: true,
instructions:
"Generate orders for a consulting company that builds websites for " +
"small businesses. Make realistic looking data, but obviously not real.",
});
const { complete } = OrderSynth.synthesize({
batchSize: 5,
count: 20,
logging: "debug",
});
const orders = (await complete()) as z.infer<typeof OrderSchema>[];
for (const o of orders) {
await firestore.collection("orders").doc(o.id).set(o);
}
return orders;
});This flow uses the OrderSchema and specific instructions to create varied and plausible order data for testing our server.
Once data exists, you'll often want to view it. We can define another flow, listOrders, to retrieve and stream all order documents from the database:
// Lists orders from firestore
ai.defineFlow("listOrders", async (_, ctx) => {
for (const ref of await firestore.collection("orders").listDocuments()) {
// stream orders
ctx.sendChunk((await ref.get()).data());
}
});These flows provide convenient ways to manage the sample data within our Genkit application during the development cycle.
3. Defining Tools
While flows orchestrate tasks, Genkit tools define specific, discrete actions. These tools are particularly important because they can be invoked by AI models or external clients like Cline. Tools often represent the core capabilities you want to expose through your MCP server.
Let's define a tool named getOrderInfo that retrieves a single order by its unique ID:
ai.defineTool(
{
name: "getOrderInfo",
description: "retrives order info",
// Define expected input using Zod schema
inputSchema: z.object({
orderId: z
.string()
.describe("Order ID. Format: A-XXX-XXX, ex. A-123-456"),
}),
// Define expected output using Zod schema (optional if order not found)
outputSchema: OrderSchema.optional(),
},
// The async function implementing the tool's logic
async (input) => {
// Assumes firestore collection 'orders' exists
const doc = await firestore.collection("orders").doc(input.orderId).get();
return doc.data() as z.infer<typeof OrderSchema> | undefined;
}
);This getOrderInfo tool takes an orderId as input, fetches the corresponding document from Firestore, and returns the data conforming to our OrderSchema.
4. Setting up the MCP Server
With the core components defined (schema, data management flows, actionable tools), you can assemble the MCP server using the genkitx-mcp plugin. This makes your Genkit flows and tools accessible via the MCP protocol.
import { mcpServer } from "genkitx-mcp";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
// Create MCP server using genkitx-mcp plugin.
// It automatically discovers tools defined on 'ai'
const mcp = mcpServer(ai, { name: "orders", version: "0.0.1" });
// Create an express server to serve the MCP server using SSEServerTransport
const app = express();
let transport: SSEServerTransport | null = null;
// Endpoint for clients (like Cline) to establish an SSE connection
app.get("/sse", (req, res) => {
transport = new SSEServerTransport("/messages", res);
mcp.server!.connect(transport);
});
app.post("/messages", (req, res) => {
if (transport) {
transport.handlePostMessage(req, res);
}
});
const port = 3000;
app.listen(port, () => {
console.log(`MCP server listening on http://localhost:${port}`);
console.log(`Connect Cline or other clients to http://localhost:${port}/sse endpoint.`);
});This setup initializes the MCP server, configures an Express application, and uses Server-Sent Events (SSE) for transport.
Now when you run the MCP server, you can access it from an MCP client like Cline and can give it tasks like: "build a website using information from order id: A-123–456".
See the demo in the video linked above (around 7:26 mark).
Conclusion
Genkit provides a robust and developer-friendly framework for building sophisticated AI-powered applications and backends. It simplifies development with features like clear data structure definition using familiar tools like Zod, straightforward orchestration of tasks through flows, the creation of discrete, reusable actions as tools, and integrated observability for monitoring traces and performance during development and in production.
By integrating seamlessly with MCP, Genkit enables these defined tools to be exposed through a standardized interface. This makes your application's capabilities readily accessible not only for direct interaction using MCP-compatible development tools (such as Cline) but also for integration into larger AI systems or agent-based architectures. Defining tools like getOrderInfo effectively creates callable endpoints for specific AI-powered business logic, ready to be consumed.
Ultimately, Genkit empowers developers to structure their AI projects effectively, manage complexity, and build interoperable components, significantly streamlining the development of modern, intelligent applications.
Genkit Discord: http://genkit.dev/discord