最新资讯
dev.to 我们在 DolphinBench 上测试记忆检索:纯关键词搜索暴露了自身回归 dev.to 别再把 Agent 写成 while 循环,把它建模为状态机 bloomberg.com Rightmove 与 Scout24 借 AI 产品应对低迷房市并上调展望 stackoverflow.blog 欢迎来到芯片时代:半导体制造、AI 数据中心与芯片设计的未来 www.youtube.com AI Agent 总犯同样的错:CentralAgent AI 将记忆重构为经验系统 pub.towardsai.net Netflix 科学家建议重采样 LLM 评判:5 次仅将误差棒缩小 13% pub.towardsai.net Jev 作为 LLM 前的静默层:构建提示感知的模型路由器 reddit.com OpenAI 称 Astra 仅满足 10% 安全任务标准 dev.to FreeToken:在自有 GPU 上运行前沿 MoE 模型及其内存需求 dev.to 当 AI Agent 说“完成”时,究竟完成了什么? dev.to 部署 Underdog Bark 3.0 Pro:逐字转写、自定义后端与分块设置 dev.to 为艰难对话构建 AI:五个 Agent、一条安全正则与一位盲评监督者
多元动态
登录 注册
AI编码 dev.to · @Lycore Development

为 Django 应用构建 MCP Server:真实项目中的经验教训

原文 我的收藏

文章介绍 MCP(Model Context Protocol)如何让应用以标准协议向 Claude、Cursor 等兼容客户端暴露工具、资源和提示,避免为每个 Agent 重复编写定制集成。作者称已为客户 Django 应用构建 MCP server 数月,本文聚焦生产代码库中的实际决策与取舍,而非 hello-world 教程。摘要仅说明 MCP 提供 JSON-RPC over stdio 或 HTTP/SSE 的标准暴露方式,未展开具体实现细节与效果数据。

AI推荐理由 来自 DEV Community 的实战经验帖,作者称已为客户 Django 应用构建 MCP server 数月,正文具体说明了 MCP 通过 JSON-RPC over stdio 或 HTTP/SSE 暴露 tools、resources、prompts 的机制,并承诺讨论生产代码库中的取舍。对开发者和 Agent 工具集成有实际参考价值;但摘要未给出具体实现细节、性能数据或可验证结果,且属个人/团队经验分享,非官方规范或独立评测,故质量分未给更高。

72

正文

MCP — the Model Context Protocol — has gone from a niche Anthropic spec to something every AI-forward team is talking about. The pitch is simple: instead of writing custom tool integrations for every agent you build, you expose your application's capabilities as an MCP server, and any MCP-compatible client (Claude, Cursor, your own agent) can use them. We have been building MCP servers for client Django applications for a few months now. This post is what we learned — not a hello-world walkthrough, but the decisions and tradeoffs that actually matter when you're doing it in a production codebase. What MCP actually gives you Before getting into implementation, it is worth being precise about what MCP does and doesn't solve. What it gives you: a standardised way to expose tools, resources, and prompts to AI clients over a defined protocol (JSON-RPC over stdio or HTTP/SSE). Instead of writing a custom function-calling schema for OpenAI, a different tool spec for Claude, and another for your internal agent, you write one MCP server and every compatible client can use it. What it doesn't give you: security, access control, rate limiting, or any business logic. MCP is a transport layer. All of that still needs to be built on top. The value is standardisation, not magic. For teams building more than one agent, or teams that want their internal tools to work with off-the-shelf AI clients, that standardisation compounds quickly. The basic structure of a Django MCP server We use the mcp Python SDK. The server lives as a standalone process that your Django application talks to — it imports Django models and services, but it is not a Django view. # mcp_server/server.py import django import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") django.setup() from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent import mcp.types as types from pydantic import BaseModel from orders.models import Order from orders.services import get_order_summary, update_order_status app = Server("myproject-mcp") class GetOrderInput(BaseModel): order_id: str class UpdateOrderStatusInput(BaseModel): order_id: str new_status: str reason: str | None = None @app.list_tools() async def list_tools() -> list[Tool]: return [ Tool( name="get_order", description=( "Retrieve full details for a specific order including line items, " "status history, and customer information." ), inputSchema=GetOrderInput.model_json_schema(), ), Tool( name="update_order_status", description=( "Update the status of an order. Valid statuses: pending, processing, " "shipped, delivered, cancelled. Requires a reason when cancelling." ), inputSchema=UpdateOrderStatusInput.model_json_schema(), ), ] @app.call_tool() async def call_tool(name: str, arguments: dict) -> list[TextContent]: if name == "get_order": params = GetOrderInput(**arguments) try: order = Order.objects.select_related("customer").prefetch_related( "line_items" ).get(order_id=params.order_id) summary = get_order_summary(order) return [TextContent(type="text", text=summary)] except Order.DoesNotExist: return [TextContent(type="text", text=f"Order {params.order_id} not found.")] if name == "update_order_status": params = UpdateOrderStatusInput(**arguments) result = update_order_status( order_id=params.order_id, new_status=params.new_status, reason=params.reason, ) return [TextContent(type="text", text=result.message)] return [TextContent(type="text", text=f"Unknown tool: {name}")] if __name__ == "__main__": import asyncio asyncio.run(stdio_server(app)) Enter fullscreen mode Exit fullscreen mode Run it with: python mcp_server/server.py The decisions that actually matter Tool descriptions are your prompt engineering surface The most important thing in an MCP server is not the code — it is the tool descriptions. This is where you communicate with the LLM that is deciding which tools to call and with what parameters. Bad description: "Get order details" Good description: "Retrieve full details for a specific order by its order ID (format: ORD-XXXXX). Returns customer name, email, line items with quantities and prices, current status, and status history. Use this before attempting to update an order status." The description tells the model when to use the tool, what inputs to provide, and what it will get back. Treat it as seriously as you would a system prompt. Scope your tools narrowly Every tool you expose is an action an agent can take. Start with read-only tools. Add write tools only when you have thought through what happens when the agent calls them incorrectly. We made the mistake of exposing a send_email tool early in one project. The agent used it in a context we did not anticipate — sending a summary email to a customer before the data it was summarising was complete. The email was not wrong exactly, but it was premature and caused a support ticket. The fix was not to remove the tool, but to add a dry_run parameter and require the agent to produce a confirmation before calling the live version. Add an audit log for every tool call Every call to a write tool should create a record: from django.utils import timezone class MCPToolCall(models.Model): tool_name = models.CharField(max_length=100) arguments = models.JSONField() result = models.TextField() caller_session = models.CharField(max_length=255, blank=True) called_at = models.DateTimeField(default=timezone.now) success = models.BooleanField(default=True) error = models.TextField(blank=True) class Meta: ordering = ["-called_at"] indexes = [ models.Index(fields=["tool_name", "called_at"]), ] Enter fullscreen mode Exit fullscreen mode Log before you execute, not after. If the tool call fails or the process dies, you want a record that the attempt was made. Handle Django's sync/async boundary explicitly The MCP SDK is async. Django's ORM is sync. You will hit SynchronousOnlyOperation errors if you call ORM queries directly from async handlers. from asgiref.sync import sync_to_async @app.call_tool() async def call_tool(name: str, arguments: dict) -> list[TextContent]: if name == "get_order": params = GetOrderInput(**arguments) get_order = sync_to_async( lambda: Order.objects.select_related("customer") .prefetch_related("line_items") .get(order_id=params.order_id) ) try: order = await get_order() summary = await sync_to_async(get_order_summary)(order) return [TextContent(type="text", text=summary)] except Order.DoesNotExist: return [TextContent(type="text", text=f"Order {params.order_id} not found.")] Enter fullscreen mode Exit fullscreen mode Alternatively, run the server with DJANGO_ALLOW_ASYNC_UNSAFE=true during development to surface the errors clearly before you fix them. Never leave it set in production. Serving over HTTP instead of stdio Stdio works well for local Claude Desktop use. For production agents that need to call your MCP server over the network, use the HTTP/SSE transport: # mcp_server/wsgi_server.py from mcp.server.sse import SseServerTransport from starlette.applications import Starlette from starlette.routing import Route, Mount transport = SseServerTransport("/messages/") async def handle_sse(request): async with transport.connect_sse( request.scope, request.receive, request._send ) as streams: await app.run(streams[0], streams[1], app.create_initialization_options()) starlette_app = Starlette( routes=[ Route("/sse", endpoint=handle_sse), Mount("/messages/", app=transport.handle_post_message), ] ) Enter fullscreen mode Exit fullscreen mode Run with uvicorn mcp_server.wsgi_server:starlette_app. Add authentication middleware before this reaches production — the MCP spec does not include auth, so you need to handle it at the transport layer. The honest summary MCP is worth adopting if you are building more than one agent or want your Django application's tools to work with multiple AI clients without rewriting integrations. The protocol is straightforward; the complexity is in the tool design and the operational concerns around write access, audit logging, and auth. The teams getting the most value are the ones treating their MCP server like an internal API: documented, versioned, with clear contracts on what each tool does and doesn't do. The teams running into trouble are the ones who exposed everything quickly and then found agents calling tools in combinations they did not anticipate. Start narrow. Add tools as you understand the usage patterns. Audit everything. Lycore builds production AI systems for businesses — MCP servers, agents, RAG pipelines, and custom LLM integrations on Django, React, Flutter, and .NET. Get in touch if you want to talk through your use case.

来源报道 · 1 篇 · 以下链接均前往原始媒体

← 返回精选动态 原文

更多内容