Introduction
Large Language Models (LLMs) are increasingly embedded into everyday systems, not just chatbots, but as agents with read/write access to enterprise tools, data, and infrastructure. However, integrating models with business logic today still feels fragmented. Each use case requires writing bespoke glue between an API, a database, a workflow engine, and your LLM provider.
Model Context Protocol (MCP) is an open specification that aims to change this. Instead of writing custom API adapters for each use case, MCP provides a unified way to expose resources (read-only context), tools (callable functions), and prompts (reusable instructions) to an AI system. It lets any LLM client speak to any server hosting business logic, securely and semantically.
In this post, I’ll dive into Python, Go, and Java (Spring Boot) implementations that can be meaningful to engineers, including querying databases and triggering outbound actions. I’ll also analyze SDK maturity, developer ergonomics, and runtime performance factors to help engineering teams choose the right starting point.
Why MCP is useful
Let’s say you want an LLM to:
- Retrieve user data from a Postgres database
- Send a marketing email
- Reference your internal refund policy as context
- Generate templated replies to customers
Without MCP, you'd need to:
- Manually ingest that context into the prompt
- Securely expose endpoints and write JSON wrappers
- Maintain schema sync between client and server
With MCP:
- You expose each of these as a resource or tool, and the model can discover, validate, and call them in a type-safe and controlled way.
Key Concepts
MCP Concept | Description | Analogy |
---|---|---|
Resource | Read-only referenceable data (e.g., docs://refund-policy ) | REST GET |
Tool | Callable function with typed input/output | REST POST |
Prompt | Pre-registered reusable template with input vars | Prompt template engine |
Server | Hosts tools/resources; communicates over STDIO/SSE/HTTP | API backend |
Client | The LLM, IDE, or agent that invokes server endpoints | Frontend/agent |
Language Comparisons
We’ll implement the same core functionality in each stack:
run_sql(query)
→rows
send_email(to, subject, html)
→"sent"
env://{key}
resource
⚠️ Note: All three implementations use the SendGrid REST API for email delivery to ensure consistency and clarity.
- Python uses the official SendGrid SDK (
sendgrid-python
) with a synchronous API call. - Go and Java use their respective SDKs or HTTP clients to build and send JSON payloads directly to the API.
Since this post isn’t a performance benchmark, each version is written to reflect idiomatic usage in its language while maintaining consistent external behavior across implementations.
Python 3.12 (FastMCP)
pip install "mcp[cli]" psycopg[binary] "sendgrid>=6" asyncpg
# server.py – Python 3.12+
from __future__ import annotations
import asyncio
import os
from typing import Annotated
import asyncpg
import sendgrid
from mcp.server.fastmcp import FastMCP
from sendgrid.helpers.mail import Mail
mcp = FastMCP("python-mcp", version="1.0.0")
pool: asyncpg.Pool | None = None # set in startup
# ── Startup / Shutdown ────────────────────────────────────────────────
@mcp.on_startup
async def startup() -> None:
dsn = os.getenv("PG_DSN")
if not dsn:
raise ValueError("PG_DSN env var not set")
global pool
pool = await asyncpg.create_pool(dsn)
@mcp.on_shutdown
async def shutdown() -> None:
if pool is not None:
await pool.close()
# ── Resources & Tools ────────────────────────────────────────────────
@mcp.resource("env://{key}")
async def env(key: str) -> str:
return os.getenv(key, "")
@mcp.tool(desc="Run a read-only SQL query against Postgres")
async def run_sql(query: Annotated[str, "SQL SELECT statement"]) -> list[dict]:
async with pool.acquire() as conn:
rows = await conn.fetch(query)
return [dict(r) for r in rows]
@mcp.tool(desc="Send an HTML email via SendGrid (sync call)")
async def send_email(
*,
to: Annotated[str, "Recipient email"],
subject: Annotated[str, "Subject line"],
html: Annotated[str, "HTML body"],
) -> str:
sg_key = os.getenv("SENDGRID_API_KEY")
if not sg_key:
raise ValueError("SENDGRID_API_KEY env var not set")
# SendGrid's official Python SDK is synchronous.
# For production-grade async, consider using `httpx.AsyncClient` to hit the SendGrid API directly.
# This example keeps it sync for demo parity across Python, Go, and Java implementations.
sg = sendgrid.SendGridAPIClient(sg_key)
resp = sg.send(
Mail(
from_email="noreply@example.com",
to_emails=[to],
subject=subject,
html_content=html,
)
)
return f"sent ({resp.status_code})"
# ── Entrypoint ────────────────────────────────────────────────────────
if __name__ == "__main__":
asyncio.run(mcp.serve_dev())
Highlights
- ️Uses
asyncpg
for high‑throughput DB access - Fastest prototyping and ease of readability
- Dev tool (
mcp dev
) includes an inspector for inspecting registered tools
2.2 Go 1.22 (mark3labs/mcp‑go)
go get github.com/mark3labs/mcp-go@latest github.com/jackc/pgx/v5
// main.go – Go 1.22 MCP server (SendGrid REST API)
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"os"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
sg "github.com/sendgrid/sendgrid-go"
"github.com/sendgrid/sendgrid-go/helpers/mail"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func main() {
ctx := context.Background()
// ── Postgres pool ────────────────────────────────────────────────
pool, err := pgxpool.New(ctx, os.Getenv("PG_DSN"))
if err != nil {
log.Fatalf("could not establish pgx pool: %v", err)
}
defer pool.Close()
// ── MCP server ──────────────────────────────────────────────────
srv := server.NewMCPServer("go-mcp", "1.0.0")
// Resource: env://{key}
srv.AddResource("env://{key}", func(_ context.Context, args map[string]any) (any, error) {
key, ok := args["key"].(string)
if !ok {
return nil, fmt.Errorf("missing key param")
}
return os.Getenv(key), nil
})
// Tool: run_sql
sqlTool := mcp.NewTool("run_sql", mcp.WithString("query", mcp.Required()))
srv.AddTool(sqlTool, func(ctx context.Context, args map[string]any) (any, error) {
rows, err := pool.Query(ctx, args["query"].(string))
if err != nil {
return nil, err
}
return pgxRowsToMaps(rows)
})
// Tool: send_email (SendGrid REST)
emailTool := mcp.NewTool(
"send_email",
mcp.WithString("to", mcp.Required()),
mcp.WithString("subject", mcp.Required()),
mcp.WithString("html", mcp.Required()),
)
srv.AddTool(emailTool, func(_ context.Context, a map[string]any) (any, error) {
req := sg.GetRequest(os.Getenv("SENDGRID_API_KEY"), "/v3/mail/send", "https://api.sendgrid.com")
req.Method = "POST"
req.Body = buildSendGridBody(a)
resp, err := sg.API(req)
if err != nil {
return nil, err
}
return fmt.Sprintf("sent (%d)", resp.StatusCode), nil
})
log.Println("MCP server listening on :8765")
if err := server.ServeHTTP(srv, ":8765"); err != nil {
log.Fatalf("serve error: %v", err)
}
}
// ── helpers ─────────────────────────────────────────────────────────
// pgxRowsToMaps converts query result rows to []map[string]any
func pgxRowsToMaps(rows pgx.Rows) ([]map[string]any, error) {
defer rows.Close()
out := make([]map[string]any, 0)
for rows.Next() {
vals, err := rows.Values()
if err != nil {
return nil, err
}
flds := rows.FieldDescriptions()
row := make(map[string]any, len(vals))
for i, f := range flds {
row[string(f.Name)] = vals[i]
}
out = append(out, row)
}
return out, rows.Err()
}
// buildSendGridBody builds the JSON payload for SendGrid
func buildSendGridBody(a map[string]any) []byte {
msg := mail.NewV3Mail()
msg.SetFrom(mail.NewEmail("noreply", "noreply@example.com"))
p := mail.NewPersonalization()
p.AddTos(mail.NewEmail("", a["to"].(string)))
p.Subject = a["subject"].(string)
msg.AddPersonalizations(p)
msg.AddContent(mail.NewContent("text/html", a["html"].(string)))
b, _ := json.Marshal(msg)
return bytes.TrimSpace(b)
}
Highlights:
- Blazing‑fast and tiny memory footprint
- Type system meshes nicely with MCP schemas
- Great for long-running services
2.3 Java 21 (Spring Boot 3.3 + Spring AI MCP)
<!-- MCP Server Spring Boot Starter -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- R2DBC for reactive Postgres queries -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-postgresql</artifactId>
<version>0.8.13.RELEASE</version>
</dependency>
<!-- SendGrid Java SDK (used for email transport) -->
<dependency>
<groupId>com.sendgrid</groupId>
<artifactId>sendgrid-java</artifactId>
<version>4.10.4</version>
</dependency>
package com.example.mcpdemo;
import com.sendgrid.*;
import com.sendgrid.helpers.mail.Mail;
import com.sendgrid.helpers.mail.objects.Content;
import com.sendgrid.helpers.mail.objects.Email;
import org.springframework.ai.mcp.resource.ResourceMapping;
import org.springframework.ai.mcp.server.annotation.McpServer;
import org.springframework.ai.mcp.tool.Tool;
import org.springframework.ai.mcp.tool.ToolCallback;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.r2dbc.core.DatabaseClient;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
@McpServer // exposes STDIO + SSE transports
@SpringBootApplication
public class McpDemoApplication {
// ── Tool: run_sql (non-blocking) ──────────────────────────────────
@Bean
@Tool(description = "Run SQL (reactive, non-blocking)")
ToolCallback runSql(DatabaseClient client) {
return req -> client
.sql(req.argument("query", String.class))
.fetch()
.all()
.collectList(); // Mono<List<Map<String,Object>>>
}
// ── Tool: send_email (SendGrid REST) ──────────────────────────────
@Bean
@Tool(description = "Send email via SendGrid REST API")
ToolCallback sendEmail(@Value("${SENDGRID_API_KEY}") String apiKey) {
return req -> Mono.fromCallable(() -> {
// Build SendGrid mail object
Email from = new Email("noreply@example.com");
Email to = new Email(req.argument("to", String.class));
String subject = req.argument("subject", String.class);
Content html = new Content("text/html", req.argument("html", String.class));
Mail mail = new Mail(from, subject, to, html);
// Execute REST call
SendGrid sg = new SendGrid(apiKey);
Request request = new Request();
request.setMethod(Method.POST);
request.setEndpoint("mail/send");
request.setBody(mail.build());
Response response = sg.api(request);
return "sent (" + response.getStatusCode() + ")";
})
.subscribeOn(Schedulers.boundedElastic()); // keeps caller thread non-blocking
}
// ── Resource: env://{key} ─────────────────────────────────────────
@Bean
@ResourceMapping("env://{key}")
String env(String key) {
return System.getenv(key);
}
public static void main(String[] args) {
SpringApplication.run(McpDemoApplication.class, args);
}
}
Highlights:
- Spring Boot Reactive Relational Database Connectivity (R2DBC) keeps I/O non‑blocking
- Spring Boot AI Starter automatically exposes your MCP server over Server-Sent Events (SSE) for web-based agents and local STDIO for command-line or IDE integrations
Recommendations
Team Profile | Start with |
Solo developer or rapid prototyper | Python (FastMCP) |
Backend team embedding into agents | Go |
Enterprise IT team with Spring AI stack | Spring Boot |
Spring AI's MCP implementation is robust for teams already invested in Java and Spring Boot. It offers a familiar, annotation-driven way to expose AI-readable business logic while integrating smoothly with enterprise infrastructure.
Python or Go MCP implementations offer faster iteration cycles and simpler tooling for projects or teams prioritizing speed, flexibility, and minimal boilerplate.
Final Thoughts
The key advantage of MCP isn’t just developer convenience - it’s strategic separation. When you decouple model access from business logic, you gain:
- Auditability: Every tool/resource call is typed, logged, and authorized
- Composability: Tools can be reused across model providers and IDEs
- Faster iteration: You change a prompt or query in one place, not five
References
Related
