๐ Kai's Task MCP Server
Turning a Spring Boot service into a full MCP surface - tools, resources, prompts, and completions
๐ฅ Demo - Claude Desktop Managing Real Tasks Over MCP

๐ง The Idea in One Line
Every other project in Spring AI taught an AI model to talk. This one teaches something different: how to make a piece of your own backend something an AI model can use - without writing a single line of custom integration code for each AI client that wants to use it.
That's the entire point of the Model Context Protocol (MCP): a standard contract so any MCP-compatible client (Claude Desktop, an IDE agent, a custom orchestrator) can discover and call tools exposed by any MCP-compatible server - no bespoke glue code per pairing.
This project takes the task-management logic from project Kai and re-exposes it as a standalone MCP server - but goes further than a tool-only server. It implements all four MCP primitives (tools, resources, prompts, completions) plus all three bidirectional server-to-client operations (elicitation, sampling, roots), using Spring AI's native MCP annotation model. No ChatClient. No LLM call of its own for the core domain logic. Just annotated Java methods any AI agent can discover, read, and be guided through.
๐๏ธ Architecture
โโโโโโโโโโโโโโโโโโโโโโโ MCP Protocol โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Claude Desktop โโโโโโ Streamable HTTP (SYNC) โโโบโ Spring AI MCP Server โ
โ (via mcp-remote โ JSON-RPC 2.0 ยท /mcp โ spring-ai-mcp-server โ
โ local bridge) โ โ port 8082 โ
โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ
annotation-scanner auto-registers
@McpTool / @McpResource / @McpPrompt
/ @McpComplete beans - no manual
ToolCallbackProvider wiring
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ โ
โโโโโโโโโโผโโโโโโโโโ โโโโโโโโโโโผโโโโโโโโโโ โโโโโโโโโโโโโโผโโโโโโโโโโโโ
โ TaskMcpService โ โ TaskMcpResources โ โ TaskMcpPrompts / โ
โ @McpTool methods โ โ @McpResource โ โ TaskMcpCompletions โ
โ + hints, context,โ โ tasks://pending โ โ daily-standup, โ
โ elicit, sample, โ โ task://{id} โ โ task-lookup + arg/uri โ
โ roots โ โ โ โ autocomplete โ
โโโโโโโโโโฌโโโโโโโโโโ โโโโโโโโโโโฌโโโโโโโโโโโ โโโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
Spring Data JPA
โ
โโโโโโโโโโโโโผโโโโโโโโโโโโ
โ PostgreSQL โ
โ tasks table โ
โโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๏ธ One path deliberately has NO protocol access:
โโโโโโโโโโโโโโโโโโโโโโโ Plain REST โ human only โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ You (curl/Postman) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโบโ DELETE /api/v1/tasks/ โ
โ Not an AI client โ TaskAdminController โ confirm-delete โ
โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ง The Full MCP Surface
MCP defines four primitives. Most portfolio MCP servers stop at tools. This one implements all four, plus the three server-initiated operations the spec allows mid-request.
Mental model: Tools = verbs the model can invoke ยท Resources = nouns the client can read for context ยท Prompts = canned instruction templates the user picks ยท Completions = autocomplete on a prompt's blanks or a resource's URI variables.
Tools (TaskMcpService)
Every tool declares readOnlyHint / destructiveHint / idempotentHint - not enforced by the runtime, but a truthful, client-facing contract about what each call actually does, so a client can decide what to auto-approve versus confirm with a human.
Notice what's missing: there is no deleteTask tool. That's not an oversight.
Resources (TaskMcpResources)
Separate from listPendingTasks on purpose - resources are for a client to browse or attach as context, not something the model is expected to actively decide to invoke.
Prompts (TaskMcpPrompts)
Completions (TaskMcpCompletions)
Bidirectional operations
- Elicitation -
completeTaskpauses mid-call to ask the human, through the client's UI, which task was meant when a title match is ambiguous. Different from the delete-gating below: this is human-in-the-loop during execution, not instead of exposing the action at all. - Sampling -
searchByKeywordasks the client's LLM to summarize results server-side. This server holds no LLM API key of its own for this - it borrows the calling client's model. - Roots -
listClientRootsreads workspace/folder context the client has exposed, demonstrating the primitive even though this domain (tasks in Postgres) has no direct filesystem use for it. - Request context logging -
createTask,completeTask, andsearchByKeywordall emitcontext.info(...)notifications mid-call, visible live in MCP Inspector / client logs, without polluting the tool's JSON schema.
All of the above are guarded with context.elicitEnabled() / context.sampleEnabled() / context.rootsEnabled() checks, since not every connecting client supports them - the server degrades gracefully instead of failing on non-interactive callers.
๐ก๏ธ Design Decision: AI Doesn't Get the Delete Button
This rule is applied on purpose:
Destructive operations are never exposed as tools an AI model can call directly.
processDeleteCompleted lets the model reason about what deletion would do and tell the user - but the actual DELETE /api/v1/tasks/confirm-delete endpoint has zero MCP exposure. It's a plain REST call a human has to make deliberately, outside the protocol entirely - a completely separate call path from the MCP tool layer, not just a permissions check bolted onto the same method.
Elicitation in completeTask is a related but distinct idea worth calling out explicitly: that's human-in-the-loop inside an allowed action (pause and ask when ambiguous), while delete-gating is human-in-the-loop by never allowing the action onto the tool surface at all. Two different trust mechanisms, same underlying belief: an LLM being confident about an action is not the same thing as that action being authorized.
Repeating this pattern across two independent projects - one built with direct tool-calling, one built on an entirely different protocol - is meant to show it's an actual engineering principle, not something that happened to work once.
๐ก What This Project Demonstrates
- Full MCP primitive coverage - tools, resources, prompts, and completions in one server, not just a tool-calling wrapper
- Server-initiated bidirectional operations - elicitation, sampling, and roots, using Spring AI's native
McpSyncRequestContext, not just request/response tool calls - Tool trust metadata -
readOnlyHint/destructiveHint/idempotentHinton every tool, a declared contract clients can act on - MCP server implementation using
spring-ai-starter-mcp-server-webmvcon the Streamable HTTP transport, stateful (SYNC) session mode - required for elicit/sample/roots to function at all - Protocol-first tool design - descriptions written for any MCP client to correctly infer intent, not tuned to one model's quirks
- Reuse across architectural styles - same underlying task domain (Kai) re-exposed through a completely different access pattern, with the safety boundary preserved
- Real client integration - connects to and is driven by Claude Desktop, not a custom-built test harness
- Protocol-level manual testing - verified the raw JSON-RPC handshake (
initializeโnotifications/initializedโtools/listโtools/call) directly in Postman before ever touching Claude Desktop, isolating protocol bugs from client bugs
๐ Project Structure
spring-ai-mcp-server/
โโโ src/main/java/com/nagarjuna/mcpserver/
โ โโโ SpringAiMcpServerApplication.java
โ โโโ entity/Task.java
โ โโโ repository/TaskRepository.java
โ โโโ service/
โ โ โโโ TaskMcpService.java โ tools: create/list/complete/search/roots/report-delete
โ โ โโโ TaskMcpResources.java โ @McpResource: tasks://pending, task://{id}
โ โ โโโ TaskMcpPrompts.java โ @McpPrompt: daily-standup, task-lookup
โ โ โโโ TaskMcpCompletions.java โ @McpComplete: keyword + id autocomplete
โ โโโ contoller/TaskAdminController.java โ human-only REST delete, no MCP access
โ โโโ exception/GlobalExceptionHandler.java
โโโ src/main/resources/application.yaml
โโโ compose.yaml โ local dev, Postgres only, auto-managed by Boot
โโโ docker-compose.full.yml โ full containerized run
โโโ Dockerfile
โโโ .env โ gitignored
No manual tool-registration config class - spring.ai.mcp.server.annotation-scanner.enabled: true auto-detects every @McpTool / @McpResource / @McpPrompt / @McpComplete bean on startup.
๐ Running It
Option A - IntelliJ + auto-managed Postgres (recommended for dev)
# Boot auto-starts compose.yaml (Postgres only) when the app runs
./mvnw spring-boot:run
Option B - Fully containerized
docker compose -f docker-compose.full.yml up --build
Server starts on http://localhost:8082, MCP endpoint at /mcp.
Critical config - without this exact block, the /mcp endpoint is never mapped and every request 404s as a static resource lookup instead of hitting the protocol handler; type: SYNC is required for elicit/sample/roots to work at all:
spring:
ai:
mcp:
server:
protocol: STREAMABLE
name: kai-task-mcp-server
version: 1.0.0
type: SYNC
annotation-scanner:
enabled: true
๐งช Testing the Protocol Directly (Postman)
Before wiring any AI client, the MCP handshake was verified manually - Streamable HTTP is stateful JSON-RPC 2.0 over a single POST endpoint, testable like any other API:
initialize- POST to/mcpwithAccept: application/json, text/event-stream. Response header carriesMcp-Session-Id.notifications/initialized- required handshake step, include the session header, expect202 Accepted.tools/list- confirms all tools register with correct auto-generated JSON schemas and hints.resources/list/prompts/list/completion/complete- confirms the non-tool primitives register too.tools/call- invoke a tool directly (e.g.createTask) and confirm the row lands in Postgres independently of the protocol response.
This step caught real bugs before they ever reached Claude Desktop: a missing protocol: STREAMABLE property, and a duplicate-Accept-header issue caused by Postman's auto-generated default headers overriding the manually-set one.
Connecting Claude Desktop
Claude Desktop's config file speaks stdio, not raw HTTP - so a local bridge (mcp-remote) is required to forward stdio traffic to the Streamable HTTP endpoint. Add to claude_desktop_config.json:
{
"mcpServers": {
"kai-task-mcp-server": {
"command": "npx",
"args": [
"mcp-remote",
"http://localhost:8082/mcp",
"--transport", "http-only",
"--allow-http"
]
}
}
}
--transport http-only and --allow-http skip SSE-fallback probing and OAuth-discovery attempts that are irrelevant for a local, unauthenticated dev server.
Windows config path: %APPDATA%\Claude\claude_desktop_config.json
Requires Node.js (npx on PATH).
Fully quit and restart Claude Desktop after editing. The task tools, resources, and prompts then appear as available capabilities - ask something like "what's still pending on my task list?" and watch it call your server, or "complete the report task" when multiple matches exist and watch it ask you which one via elicitation.
๐ Database
Reuses the same tasks table shape as Project Kai. One Postgres instance, one schema, no vector store or chat memory tables involved this time - this project has nothing to do with embeddings or conversation history, deliberately.
๐ญ Not Yet Done
- STDIO transport variant (currently Streamable HTTP only)
- Stateless server mode as an explicit alternative (would simplify scaling but drop elicit/sample/roots support - documented as a known trade-off, not pursued here since demonstrating the bidirectional operations was the point)
- MCP client module to consume external MCP servers from this same codebase
- Automated integration tests against the MCP protocol layer
- MCP Security / OAuth2 resource-server auth for public exposure - out of scope for this local-dev-focused iteration, natural next hardening step once the module stabilizes
Built by S Nagarjuna