Build a Dart ADK Agent and MCP Server
Dart developers do not need a Python or Node.js service just to experiment with agents and Model Context Protocol (MCP) tools. This project uses adk_dart for the agent and shelf for a small HTTP server that exposes an MCP-compatible greeting tool over Server-Sent Events (SSE).
The complete code is in the adk-hello-world-dart repository.
Note- the Dart library is not an official ADK. An official Dart ADK has not been released as of July 2026. This approach provides an alternative to start working with agents in Dart without waiting for an official SDK.
What the sample contains
The repository has two related examples:
-
bin/main.dartcreates anLlmAgentand registers an ADKFunctionTool. -
bin/server.dartstarts a Shelf server with an SSE endpoint and a JSON-RPC message endpoint.
The server implements the MCP methods needed by this demo: initialize, notifications/initialized, ping, tools/list, and tools/call. The transport and JSON-RPC routing are deliberately small and live in SessionService; they are not a general-purpose MCP server implementation.
flowchart LR
Client[MCP client] -->|GET /sse| Server[Shelf server]
Server -->|endpoint event| Client
Client -->|POST /messages?sessionId=...| Server
Server --> Session[SessionService]
Session --> Tool[greet tool]
CLI[Dart CLI] --> Agent[ADK LlmAgent]
Agent --> ADKTool[ADK FunctionTool]
1. Add the dependencies
The current project targets Dart 3.5 or later and uses these package versions:
environment:
sdk: ^3.5.0
dependencies:
adk_dart: ^2026.7.24
adk_mcp: ^2026.7.24
logging: ^1.3.0
shelf: ^1.4.1
shelf_router: ^1.1.4
uuid: ^4.5.1
Install them with:
dart pub get
adk_dart is used directly by the sample agent. The repository also tracks adk_mcp, while the current server keeps its MCP transport explicit in SessionService so the protocol flow is easy to inspect.
2. Define the greeting tool
The project keeps the greeting logic separate from its ADK and MCP wrappers:
class Tools {
static String formatGreeting(String name) {
return 'Hello, $name!';
}
static final FunctionTool greetFunctionTool = FunctionTool(
name: Config.toolGreet,
description: 'Get a greeting from a local HTTPS server.',
func: ({String? param}) {
final name = param ?? 'World';
return formatGreeting(name);
},
);
}
Tools also exposes an MCP tool definition with a JSON Schema input named param. Keeping formatGreeting as a plain Dart function makes the domain behavior easy to unit test.
3. Create the ADK agent
AdkGreetingAgent attaches the function tool to an LlmAgent:
class AdkGreetingAgent {
static LlmAgent createAgent() {
return LlmAgent(
name: 'GreetingAgent',
description: 'An AI Agent built with adk_dart that provides greetings.',
instruction:
'You are a friendly greeting assistant. '
'Use the greet tool to provide personalized greetings.',
tools: [Tools.greetFunctionTool],
);
}
}
Run the CLI example to confirm that the agent and tool can be created:
dart run bin/main.dart
This command initializes the agent and prints a sample tool result. It does not call a hosted model.
4. Expose the MCP endpoints
The Shelf server registers four routes:
router.get('/', (request) => Response.ok('ADK & MCP Dart Server Running'));
router.get('/health', (request) => Response.ok('OK'));
router.get(Config.sseEndpoint, sessionService.handleSseSession);
router.post(Config.messagesEndpoint, sessionService.handlePostMessage);
When a client opens GET /sse, SessionService creates an in-memory session and sends an endpoint event containing a URL such as:
/messages?sessionId=7c6d...
The client posts JSON-RPC requests to that URL. Responses arrive as message events on the original SSE connection.
Start the server with:
dart run bin/server.dart
It listens on port 8080 by default. Set the PORT environment variable to use another port.
5. Connect an MCP client
For an MCP client that supports remote SSE servers, point it at:
http://localhost:8080/sse
A typical client configuration looks like this:
{
"mcpServers": {
"dart-greeting-server": {
"url": "http://localhost:8080/sse"
}
}
}
Configuration keys differ between MCP clients, so check the documentation for the client you use. Once connected, call the greet tool with:
{
"param": "Dart developer"
}
The result is Hello, Dart developer!.
6. Test and verify the project
The repository includes unit tests for the greeting behavior and endpoint tests for the Shelf server:
dart test
dart analyze
You can run the complete build, analysis, and test sequence with:
make check
7. Build the container
The included multi-stage Dockerfile compiles the server to a native executable and copies it into a small scratch image:
docker build -t adk-hello-world-dart .
docker run --rm -p 8080:8080 adk-hello-world-dart
Check the running container at http://localhost:8080/health.
8. Deploy to Cloud Run
cloudbuild.yaml builds the image, pushes it to Container Registry, and deploys the service in us-central1:
make deploy
The deployment allows unauthenticated access and sets --max-instances 1.
That instance limit matters here. Active SSE transports are stored in a process-local map, so a POST request routed to another instance would not find its session. A production service should move session state to shared storage or use a transport and deployment design that does not depend on process-local routing. Authentication, origin restrictions, request validation, timeouts, and rate limiting would also need attention before exposing the service publicly.
Where to go next
This sample is intentionally narrow: one agent, one deterministic tool, and enough MCP handling to show the request flow. Useful next steps include replacing the greeting with real domain logic, using adk_mcp transport primitives as the Dart package evolves, adding model configuration to execute the agent, and moving session state out of memory before scaling the service.
Resources:
Top comments (17)
The process-local map is also a security boundary, not only a scaling constraint. In this shape,
sessionIdis effectively a bearer capability carried in a URL; URLs are routinely copied into access logs and traces, so randomness alone should not authorize a POST to an existing SSE channel.Before replacing the greeting with a real tool, I would bind each session to an authenticated principal and client identity, apply a short idle and absolute TTL, delete it on disconnect, cap sessions and in-flight calls per principal, and keep the query string out of logs. The POST handler should reject a valid session ID presented by the wrong principal even if the ID leaked.
Useful endpoint tests would cover unknown, expired, foreign, replayed, and concurrently used session IDs, plus container restart between
GET /sseandPOST /messages. Those cases make the demo's boundary explicit:--max-instances 1prevents cross-instance routing, but it does not make process memory durable or a session URL confidential.main caveat is that is not an official ADK library. I have no idea if or when an official Dart ADK will be released. If an official one hits the repos then the plan is to rework the article
Spot on. That sessionId leakage is exactly a Mechanical layer failure. It doesn't matter how good your Semantic LLM reasoning is if the transport layer hands a valid session to the wrong principal. Your suggestion to bind sessions to authenticated identities is the only safe way to cross that boundary.
The useful thing about pairing an agent with MCP is that it forces the integration boundary to become explicit. The server is not just plumbing; it is where permissions, tool shape, failure behavior, and auditability can live outside the agent prompt. That separation pays off quickly.
main caveat is that is not an official ADK library. I have no idea if or when an official Dart ADK will be released.
Good caveat. That is exactly the kind of detail that should be visible in the integration docs: official SDK, community library, or experimental bridge. The architecture can still be useful, but the operational risk is very different if the library is not on the official support path.
This is exactly the part I found interesting about MCP.
One additional lesson from building agent tooling: making the boundary explicit solves where the checks can happen, but not automatically whether the final conclusion is correct.
I tend to separate three layers:
The dangerous failures often happen between the second and third layer. The tool can succeed, the receipt can be valid, and the agent can still draw the wrong conclusion.
For me, MCP is valuable because it gives us a clean place to keep the first two layers deterministic, leaving the model to handle only the part that actually requires reasoning.
I didn't want to dig too deep as the ADK is unofficial. I found it as a Dart library. I don't know the official rollouts so a fully supported version from Google may be in the works.
Totally understandable. The library status aside, the architectural pattern you demonstrated is solid. It’s exactly that clean boundary (that alexshev mentioned above) that makes testing the mechanical vs semantic layers possible. Thanks for putting the sample together, it's a great starting point for the Dart ecosystem.
Nice writeup — the explicit separation between the ADK agent layer and the MCP transport is the part worth paying attention to.
Building on Mikhail's three-layer framing (mechanical / evidence / semantic): one useful extension might be treating tool schema as a first-class contract, not just the tool output. If two agents call the same tool with subtly different input shapes and get silently different results, the mechanical layer says "success" but the semantic layer drifts. MCP's JSON Schema input definitions are already there — surfacing schema diffs in test output could catch a whole class of integration failures before they reach the reasoning layer.
The process-local session limitation is a good design constraint actually — it makes the demo's boundaries honest. Looking forward to seeing how this evolves when session state moves to shared storage.
Any plans to add streamable HTTP? The MCP spec deprecated the SSE transport a while back and newer clients are drifting that way, so the endpoint shape might age faster than the unofficial ADK part does.
so far just basic proof of concept. I don't know official plans but it would seem that dart is a logical path for an official ADK
Nice to see an ADK build in Dart, the SSE plus Cloud Run path is usually where the rough edges show up. One thing I would add before this leaves the toy stage: a check on what the MCP tool actually returns, since a server can change its schema or hand back junk and the agent will keep calling it happily. Did you hit any cold-start weirdness with the MCP connection on Cloud Run?
Not directly on this one - but I have seen it on other projects. if you run with scale to 0 at some point you will get hit with a cold start penalty. Measuring that building that goes beyond just dart/MCP
Great walkthrough. I especially like that the article points out the difference between a working demo and a production-ready MCP service. The process-local session map is easy to understand for a small example, but authentication, session expiration, request validation, and protection against session-ID leakage become important as soon as the server is exposed beyond localhost.
Another useful improvement would be to add structured logging around
[tools/call](yazistil.com.tr/) requests without logging sensitive session information so developers can distinguish transport errors, tool execution failures, and incorrect model reasoning. Keeping the tool layer deterministic while leaving semantic decisions to the agent seems like a solid architecture for experimenting with MCP in Dart.Some comments may only be visible to logged-in visitors. Sign in to view all comments.