Model Context Protocol: A Universal Adapter for AI Agents and LLMs

ℹ️
A comparison of MCP implementations in Python, Go, and Java Spring Boot with practical examples and tradeoffs.

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 ConceptDescriptionAnalogy
ResourceRead-only referenceable data (e.g., docs://refund-policy)REST GET
ToolCallable function with typed input/outputREST POST
PromptPre-registered reusable template with input varsPrompt template engine
ServerHosts tools/resources; communicates over STDIO/SSE/HTTPAPI backend
ClientThe LLM, IDE, or agent that invokes server endpointsFrontend/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 ProfileStart with
Solo developer or rapid prototyperPython (FastMCP)
Backend team embedding into agentsGo
Enterprise IT team with Spring AI stackSpring 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
‼️
As LLMs become more agentic, your business logic needs to be AI-readable. That’s exactly what MCP does.

References

Cursor – Model Context Protocol
Connect external tools and data sources to Cursor using the Model Context Protocol (MCP) plugin system
Comparing Model Context Protocol (MCP) Server Frameworks
Introduction The Model Context Protocol (MCP) is a new standard for connecting AI assistants (like LLMs) with external data sources and…
Building a Serverless remote MCP Server on AWS - Part 1
Build a serverless MCP server on AWS Lambda and Amazon API Gateway that any LLM can access remotely