Skip to content

feat(mcp): expose DICE tools for MCP server integration - #52

Merged
jimador merged 8 commits into
embabel:mainfrom
LordKay-sudo:feat/mcp-server-tools
Sep 10, 2026
Merged

jimador merged 8 commits into
embabel:mainfrom
LordKay-sudo:feat/mcp-server-tools

Conversation

@LordKay-sudo

@LordKay-sudo LordKay-sudo commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add DiceMcpTools with simplified context_id-scoped parameters for external MCP clients (Claude Desktop, Cursor, etc.).
  • Add dice-mcp-autoconfigure to export those tools via embabel-agent McpToolExport when embabel.dice.mcp.enabled=true.
  • Four tools only: dice_recall, dice_list, dice_store, dice_get. get refuses an id that belongs to another context.

Fixes #5

Motivation

Issue #5 asks to expose DICE tools, perhaps with simplified parameters. External MCP clients are stateless, so every tool takes an explicit context_id. In-process Memory / DiscoveryTools still bake context in at construction; REST still takes it from the URL. MCP cannot do either, so isolation is a per-call check plus PropositionQuery.forContextId.

Scope

This only exports DiceMcpTools. DiscoveryTools and GraphQueryTools stay on the in-process asTools() path. Extraction stays on PropositionPipeline.

What changed

Area Change
com.embabel.dice.mcp DiceMcpTools, DiceMcpSupport
dice-mcp-autoconfigure Opt-in Spring Boot wiring + named diceMcpToolExport bean
Docs House-aligned README / architecture / AGENTS.md notes

Test plan

  • DiceMcpToolsTest: store/get, recall, list filters, context isolation, constructor guards
  • DiceMcpAutoConfigurationTest: opt-in, missing store, property bind, custom beans, ConditionalOnClass
  • full mvn verify (all modules, zero skipped tests)

@LordKay-sudo
LordKay-sudo force-pushed the feat/mcp-server-tools branch from 993700f to 31f9763 Compare June 27, 2026 13:46
@LordKay-sudo
LordKay-sudo force-pushed the feat/mcp-server-tools branch from 31f9763 to 95f027f Compare July 10, 2026 09:44
@LordKay-sudo

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main (through #60). Fixed the parent version to 0.1.1-SNAPSHOT. Build and DCO are green again. Ready when you are.

@LordKay-sudo
LordKay-sudo force-pushed the feat/mcp-server-tools branch from 95f027f to 2b40686 Compare July 10, 2026 12:52
@igordayen

Copy link
Copy Markdown

@LordKay-sudo - how are you? DICE is undergoing refactoring; could you please try to rebase? Thank you
looping @jimador

@LordKay-sudo

LordKay-sudo commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Hallo, I'm well thanks. Rebased onto current main and checked it against 0.2 / agent 1.5.

Same scope: context-scoped DiceMcpTools and opt-in autoconfig. Cut extract/assert and the core/extended profile so this stays four tools. Discovery/graph tools still out of this PR.

@igordayen

igordayen commented Sep 5, 2026

Copy link
Copy Markdown

From CODEX:

  • P2 dice/src/main/kotlin/com/embabel/dice/mcp/DiceMcpTools.kt:68: guarded() lets every IllegalArgumentException through. That includes exceptions thrown by repository.save/query/findById, not just caller validation. Example: if Neo4j/repository code throws IllegalArgumentException("MATCH ... bolt://neo4j-prod..."), that text is returned to the MCP client, despite the comment saying store details are sanitized. The tests only cover RuntimeException, so they miss this. Prefer a small internal validation exception, or validate inputs outside the guarded store block and sanitize all repository exceptions.

  • P2 dice/src/main/kotlin/com/embabel/dice/mcp/DiceMcpTools.kt:187: tool names are fixed as dice_recall, dice_list, dice_store,
    dice_get, and dice-mcp-autoconfigure/.../DiceMcpAutoConfiguration.kt:82 exports them without a naming strategy. Embabel’s MCP
    export path deduplicates by final tool name, so if a host exposes another DICE tool set or another package also has dice_get, one tool can silently disappear, or the model cannot target the right one. Use a configurable/export-level prefix, e.g., embabel_dice_ or an app-specific prefix, via ToolObject naming strategy.

  • P3 dice/src/main/kotlin/com/embabel/dice/mcp/DiceMcpSupport.kt:111: "%.2f".format(...) uses the JVM default locale. Under a locale
    with comma decimals, the MCP output can become confidence=0,90 instead of confidence=0.90. Use Locale.ROOT so tool output stays stable across deployments.

@igordayen igordayen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LordKay-sudo - few comments from my side and codex. Please also be aware of PR
embabel/embabel-agent#1833 on tool name uniqueness.
Please add comments to this PR, if you see better approaches, rather than Agent_Tool.
Thank you

*
* In-process [com.embabel.dice.agent.Memory] and [com.embabel.dice.agent.DiscoveryTools] bake
* [ContextId] in at construction, so an agent cannot name another tenant. MCP clients are
* stateless and may serve many sessions, so every tool takes an explicit `context_id`. That is

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think stateless MCP requires the model to choose a context. With this shape, tenant selection becomes model-generated input and every host has to build authorization around it. Could we expose a host-supplied context policy, with a fixed-context implementation for the common case and an authorizing resolver for multi-tenant servers? Then the tool operates on a resolved ContextId, and caller-supplied context remains an explicit policy choice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. A model-chosen tenant should not be the default. A host policy (fixed context for the common case, an authorizing resolver when the host is multi-tenant) is the right shape. The tools then run on a resolved ContextId.

Do you want that policy in this PR, or should I land the schema, sanitizer, and write-switch fixes first and do the policy next?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want that policy in this PR, or should I land the schema, sanitizer, and write-switch fixes first and do the policy next?

@LordKay-sudo - please assess the number of additional artifacts involved. This PR is already medium-size, thank you

@LordKay-sudo LordKay-sudo Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@igordayen Counted it. A host context policy is a new SPI, two implementations, autoconfig properties, schema changes on all four tools, a new test class plus rewrites of the isolation tests and the MCP docs. Callers currently have to pass contextId; a fixed-context host would take that off the model. That is a second public contract in this PR.

I would land this one as is and do the policy next.

@jimador jimador Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @LordKay-sudo, that’s fair. Let’s keep this scoped and follow up on context policy once we have a concrete client integration to drive it. My concern about model-selected context still stands, but I don’t think we need to design the full abstraction here.

Comment thread dice/src/main/kotlin/com/embabel/dice/mcp/DiceMcpTools.kt Outdated
Comment thread README.md Outdated
Comment thread dice/src/main/kotlin/com/embabel/dice/mcp/DiceMcpSupport.kt
@LordKay-sudo

Copy link
Copy Markdown
Contributor Author

@igordayen dice_recall, dice_list, dice_store, and dice_get are a public contract. Qualifying them as Agent_Tool changes every MCP client for a collision that is two exports sharing a final name.

I would keep the dice_* names. If a host publishes more than one tool set, ToolObject.withPrefix is already on the 1.5 export path we compile against. That is the place for uniqueness. Collision should be reported, not solved by rewriting the name the client already uses.

Agent_Tool also gets hard to read once dots are sanitized. A host prefix stays something a person can type.

@LordKay-sudo

Copy link
Copy Markdown
Contributor Author

@igordayen The IAE hole is real. I will validate inputs outside guarded so a store IllegalArgumentException is sanitized. Locale will use Locale.ROOT.

MethodTool already logs the throwable. I will drop the second stack from guarded.

@igordayen

Copy link
Copy Markdown

@LordKay-sudo - there are conflicts on the branch. Also, could you please mark it as "resolved" upon resolution? Thank you

)

Context-scoped DiceMcpTools (recall, list, store, get) and opt-in
dice-mcp-autoconfigure. MCP clients pass context_id per call; get refuses
cross-context ids. Discovery and extraction stay off this surface.

Signed-off-by: LordKay-sudo <lkandiro@gmail.com>
MCP clients often pad copied ids; lookup should match the stored id.

Signed-off-by: LordKay-sudo <lkandiro@gmail.com>
afterName is a string so production stays free of a storage compile dep.
The combined runner lists MCP first so a broken name cannot hide behind declaration order.

Signed-off-by: LordKay-sudo <lkandiro@gmail.com>
dice_get advertises an id "returned by recall, list, or store", but recall
rendered through MemoryRetriever, which never emits one, so the documented
recall-then-get flow could not actually be performed. Split ranking from
rendering in MemoryRetriever and let the MCP surface format hits with the
same helper dice_list uses; all four tools now share one output shape.

limit was only coerced upward. An MCP caller is external and its limit is
whatever the client model wrote, so clamp it to 100, mirroring the MAX_TOP_K
RetrievalRouter already applies before it does any work. default-limit is
validated against the same ceiling so a larger value fails startup instead
of being silently truncated on every call.

Signed-off-by: LordKay-sudo <lkandiro@gmail.com>
A live store or driver error used to reach the MCP client as exception
text. Guard the four tools the way DiscoveryController sanitizes its 500s:
caller validation still throws IllegalArgumentException, everything else
becomes a cause-free generic failure so Cypher and bolt hosts cannot leak.

get now answers a missing id and a foreign-context id the same way
MemoryController collapses both into a 404, so the tool cannot confirm
that an id it does not own exists.

A query miss also restores the in-scope count and retry nudge that
MemoryRetriever used to provide, without putting a raw context id into
a slot meant for a human-readable topic.

Signed-off-by: LordKay-sudo <lkandiro@gmail.com>
MCP clients can name any tenant; the per-call check only stops one
call from crossing contexts. Authorization is the host MCP server's
job. Also note that dice_store writes a fact with no mentions or
provenance, so it is retrievable by vector and keyword only.

Signed-off-by: LordKay-sudo <lkandiro@gmail.com>
Three modules pinned 5.4.0 locally. One property, one
dependencyManagement entry.

Signed-off-by: LordKay-sudo <lkandiro@gmail.com>
Optional tool fields were required in the published schema, docs used
snake_case names the binder does not accept, and a store
IllegalArgumentException could still reach the client. Mark query,
limit, and confidence optional, document contextId/propositionId,
sanitize every repository exception, and keep dice_store off unless
writes-enabled is set. Get now shows status so a stale fact does not
look active.

Signed-off-by: LordKay-sudo <lkandiro@gmail.com>
@LordKay-sudo

Copy link
Copy Markdown
Contributor Author

@igordayen Rebased onto current main. Schema, writes, and get-detail threads are marked resolved. Left James's host context-policy thread open.

@igordayen igordayen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LordKay-sudo - looks good; thank you for contributing!

@LordKay-sudo

LordKay-sudo commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

@igordayen Thanks.

@jimador
jimador merged commit 870b9ab into embabel:main Sep 10, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make it easy to expose an MCP server

3 participants