Skip to content

feat: dial-unified-config — MCP server (admin tools for read/write/upload/publish)#1530

Open
siarhei-fedziukovich wants to merge 29 commits into
feature/unified-config-serverfrom
feature/unified-config-mcp
Open

feat: dial-unified-config — MCP server (admin tools for read/write/upload/publish)#1530
siarhei-fedziukovich wants to merge 29 commits into
feature/unified-config-serverfrom
feature/unified-config-mcp

Conversation

@siarhei-fedziukovich
Copy link
Copy Markdown
Contributor

  • docs(dial-unified-config): rewrite 09-mcp-spec as unified building-block v0.2
  • docs(dial-unified-config): tighten 09-mcp-spec M4 wording and summary projections
  • docs(dial-unified-config): drop 09-mcp-spec Success Metrics section
  • docs(dial-unified-config): pivot 09-mcp-spec to v0.3 in-repo Java module
  • docs(dial-unified-config): resolve all 8 MCP-OQs in 09-mcp-spec (v0.4)
  • mcp spec polishing
  • feat: M.0-pre: bootstrap :mcp Gradle module + /mcp 503 stub
  • docs: M.0-pre: backfill commit SHA in §5.6 register
  • feat: M.0.0-bridge: wire Vert.x ↔ MCP-SDK Streamable HTTP transport
  • docs: M.0.0-bridge: backfill commit SHA in §5.6 register
  • feat: M.0.1-pre: lock captured-context bridge and add DialClient loopback wrapper
  • docs: M.0.1-pre: backfill commit SHA in §5.6 register
  • feat: M.0.2-pre: per-session rate-limit and concurrency cap for MCP
  • docs: M.0.2-pre: backfill commit SHA in §5.6 register
  • feat: M.1.0: read tools bootstrap for DIAL MCP — describe_schema, list, get
  • docs: M.1.0: backfill commit SHA in §5.6 register
  • feat: M.1.1: read-tools entity-type sweep — full 12-type catalog + hierarchical envelope
  • feat: M.2.0: bootstrap dial_create/update/delete_resource MCP write tools
  • docs: M.2.0: mark slice ✅ and capture write-tools retrospective
  • docs: M.1.1: backfill commit SHA + slice retrospective in §5.6 register
  • feat: M.2.1: extend MCP write tools to settings + files DELETE specials
  • docs: M.2.1: mark slice ✅ and capture write-tools sweep retrospective
  • chore: insert M.3.1-handshake-readiness slice (Track C)
  • feat: M.3.1-handshake-readiness: defer /mcp dispatch until McpVerticle ready
  • docs: M.3.1: mark slice ✅ and capture handshake-readiness retrospective
  • feat: M.3.0: ship dial_upload_file + dial_download_file MCP tools
  • docs: M.3.0: mark slice ✅ and capture file-tools retrospective
  • feat: M.4.0: ship dial_publish_resource MCP tool
  • docs: M.4.0: mark slice ✅ and capture publication-tool retrospective

Applicable issues

  • fixes #

Description of changes

Checklist

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

SiarheiFedziukovich and others added 29 commits May 11, 2026 19:31
…ock v0.2

Collapse the admin/user split into a single dial_* surface (Core enforces
authz by caller identity), shrink to 9 building-block tools, and reframe
stack/deployment as an external Python sidecar (FastMCP/mcp SDK). Two-array
list envelope handles flat and hierarchical types uniformly; format:summary
on list, detailed on get; bucket aliases (private/public/platform) resolved
server-side via cached /v1/bucket. Audit, apply, diff/export,
effective-policy, codeapp, dial-api adoption, OpenAPI gen, and
DIAL-app-with-MCP move to a §12 future-work register with unlock
conditions. README cross-references updated to drop the "Admin MCP" and
"raw draft" framing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… projections

M4 no longer asks tool descriptions to embed REST-equivalent details — that
inflates every agent's context for a fallback path that doesn't pay off.
Keep example invocations only.

Summary-projection table: drop iconUrl (models, applications) and
updatedAt (files, prompts, conversations); add description across all
entity rows (singleton settings excluded). Description is the highest-
signal summary field for an agent picking between candidates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove §9 Success Metrics — KPI targets at this stage are speculative and
the eval-driven-development guidance can re-land if/when it has a real
home. Renumber subsequent sections (Risks → §9, Open Questions → §10,
Future Work → §11, References → §12) and the inline §12 cross-references
that pointed at the future-work register.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverse the v0.2 external-Python-sidecar shape in favor of an in-repo
Gradle module embedded in DIAL Core as a Vert.x verticle for v1 delivery
speed. Add explicit extraction discipline (§7.1) — REST-only access to
Core via loopback HTTP, dependency-graph CI check, config-driven Core URL,
own verticle/thread-pool, tokens forwarded verbatim — so future extraction
to a standalone service is a build-and-deploy change rather than a
refactor. Stack pivots to Java for code reuse with config/ POJOs and
Jackson; Python preserved as a post-extraction option.

Updates: §1 summary, §7 architecture (module placement, stack rationale,
deployment shapes), §8 phasing (HTTP-only first cut), §9 risks (release-
cadence coupling and discipline-erosion replace the separate-repo drift
risk), §10 OQs (drop repo-name, add endpoint-placement and stdio-launcher
questions), §11 future work (add extraction trigger entry plus L2 Core-
embedded MCP capabilities entry), §12 references (Java SDK first), Next.
README cross-reference updated to mention the embedded-module v1 shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump to v0.4. Resolutions:
- OQ-1 module name: mcp/ (matches sibling-module convention)
- OQ-2 summary projections: ship table as-is, revisit on eval data
- OQ-3 multi-env: pin each MCP instance to one env; cross-env is
  agent-side composition across two MCP connections
- OQ-4 schema caching: not needed in v1 — schemas are in-process
  registry lookups loaded at JVM startup from the same config/
  Gradle module Core uses; cache discussion deferred to extraction
- OQ-5 destructive UX: confirm:true is enough; two-step flow is L2
- OQ-6 observability: rely on Core logs + agent traces, no /metrics
- OQ-7 endpoint placement: mount on Core's existing port at /mcp
- OQ-8 stdio: punt — v1 is HTTP/SSE-only

Threaded through the doc: M9 reframed (in-process registry, runtime
fetch is post-extraction fallback), §7.2 transport line resolves
OQ-7/OQ-8, §7.3 deployment table drops the stdio row, §8 phasing
clarifies one-MCP-per-env, §11 dial_diff_environments / dial_export
unlock conditions reframed now that OQ-3 is resolved, N4 rewritten
as a plain assertion. §10 OQ table strikethroughs the originals
with inline Resolved: notes for traceability. §Next replaced with
implementation-ready next steps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Track C foundation: new :mcp sibling Gradle module (:config dep + Vert.x
core only), McpVerticle skeleton, McpRequestHandler returning 503 transport-not-
wired, Proxy /mcp short-circuit (404 when mcp.enabled=false per §7.1 kill-switch),
mcp.* settings defaults per §7.1, and CONTRIBUTING.md documenting extraction
discipline. SDK dep + Vert.x↔MCP-SDK transport adapter carved into sibling slice
M.0.0-bridge (start-of-slice halt resolution).

Design anchors: 09 §1, §7.1, §7.2, §8 kickoff checklist
Tests: mcp/.../McpRequestHandlerTest.java, server/.../McpRoutingTest.java, server/.../McpDisabledRoutingTest.java

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements VertxMcpTransportProvider against the SDK's
McpStreamableServerTransportProvider SPI; bridges HttpServerRequest
body buffering, executeBlocking-isolated SDK Mono blocks, and
runOnContext-marshalled SSE writes. Replaces M.0-pre's 503 stub.
Zero tools registered — the tool surface lands in M.1.x.

Design anchors: 09 §7.1, §7.2, §8 kickoff checklist
Tests: server/src/test/java/com/epam/aidial/core/server/McpHandshakeTest.java
       + mcp/src/test/java/com/epam/aidial/core/mcp/transport/VertxMcpTransportProviderTest.java

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…back wrapper

Picks option (a) captured-context dispatch from spec §7.2: DialClient encapsulates
the Reactor->Vert.x bridge via Mono.create + runOnContext + onComplete, so M.1.x
tool handlers never touch context plumbing directly. Loopback URL resolves env
MCP_DIAL_TARGET_URL > settings mcp.dialTargetUrl > http://localhost:8080.
McpVerticle.start() captures the verticle context once and constructs DialClient.

Design anchors: 09 §7.1 (rule 3), §7.2 (bridge option a), §7.4 (verbatim auth)
Tests: mcp/src/test/.../client/DialClientTest.java; server/src/test/.../McpDialClientLoopbackTest.java

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds in-memory token-bucket per MCP session (60 calls/min, burst 10) and a
5-call concurrency cap, enforced inside VertxMcpTransportProvider.dispatchPost
because SDK 1.1.2 does not expose session-id at tool-handler time. Overflow
returns a JSON-RPC error (code -32000, data.retry_after) marshalled to the
event loop. Decoration point chosen at transport, not DialClient. Fixed-point
math with pre-clamp guards against long-overflow on idle sessions.

Design anchors: 09 §7.1 (M10), §9 risk row 1
Tests: mcp/src/test/.../ratelimit/McpSessionLimiterTest.java (10 tests)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mark M.0.2-pre as ✅ with squash commit 31cd5e4. Enrich the slice row with
the option-α decision (transport-layer enforcement vs DialClient decoration),
JSON-RPC error shape, retry_after=1 floor, the long-overflow fix from review,
and the deferred end-to-end integration test boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t, get

Wires three MCP read tools onto the existing :mcp module: dial_describe_schema (in-process
Victools-built JSON Schema lookup per spec §M9, no HTTP), dial_list_resources (two-array
envelope with summary projection per §6.3-§6.4), dial_get_resource (entity body augmented
with etag — null for config GETs in Phase 1, pre-existing Core gap). Pilot pattern proven on
models/roles/settings; M.1.1 mechanically extends to remaining 9 types. Auth model
collapsed: caller's Api-Key/Authorization headers forwarded verbatim via McpTransportContext
(no env var). SDK 1.1.2's exchange.sessionId() is public — used as the per-session
bucket-cache key, with success-only Mono caching to avoid error replay on transient
GET /v1/bucket failures.

Design anchors: 09 §1, §6.1-§6.4, §7.4-§7.5, §M9
Tests: mcp/.../{schema,tools}/*Test.java; server/.../McpReadToolsTest.java

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…erarchical envelope

Expands dial_list_resources / dial_get_resource from the M.1.0 pilot
(models, roles, settings) to all 12 spec types — applications, toolsets,
interceptors, keys, routes, schemas, files, prompts, conversations now
listable / gettable through the MCP read surface. dial_describe_schema is
unchanged (M.1.0 already covered the 8 :config POJOs + DIAL meta-schema;
files/prompts/conversations stay as not-implemented envelope per §M9).

Per-type Core path routing centralized in ResourceId: config types hit
/v1/{type}/{bucket}/...; applications, toolsets, files, prompts,
conversations route listings through /v1/metadata/{type}/{bucket}/...;
files additionally route individual GETs through /v1/metadata/... since
/v1/files/{bucket}/{path} returns raw bytes (deferred to dial_download_file
in M.3.0). Hierarchical-types envelope splits the upstream
ResourceFolderMetadata response into the spec §6.3 two-array shape: items[]
for nodeType=ITEM, folders[] for nodeType=FOLDER, with nextToken mapped to
nextCursor. recursive=true and cursor are rejected on flat config types
with structured remediation hints (Core's config-resource controller is
single-page-no-cursor). settings keeps its 405-on-list short-circuit from
M.1.0.

SUMMARY_FIELDS table populated for all 12 types per spec §6.4; metadata-
derived items (apps/toolsets/files/prompts/conversations) silently no-op
fields the metadata response doesn't carry — N+1 enrichment is post-MVP
explicit defer.

Reviewer-driven fixes (pre-merge): (HIGH) cursor was silently dropped for
flat types — added cursorNotSupported error guard mirroring the
recursive-on-flat rejection; (HIGH) envelope path field used the alias
bucket while child ids used the resolved bucket — both now use the
resolved bucket so listings always return canonical ids per spec §6.2.

SIMPLIFY pass folded 8 fixes: shape() slimmed from 6 args to (resp,
ResourceId, resolvedBucket, format); summaryFields() drops the defensive
copy on the immutable List.of values; parentPath() extracted to deduplicate
projectFolder/projectItem null/empty handling; Jackson path() replaces
has()+get() doubled probes; appendQuery dropped its LinkedHashMap;
usesMetadataList renamed to supportsRecursive; milestone-narrating javadoc
replaced with stable contract docs; toCorePath / toListCorePath javadoc
nails the parse / parseListPath pairing rule.

Design anchors: 09 §6.3 (two-array envelope), §6.4 (summary projection)
Tests: mcp/src/test/java/com/epam/aidial/core/mcp/tools/{ResourceIdTest,
       ListResourcesEnvelopeTest, SummaryFieldsTest}.java;
       server/src/test/java/com/epam/aidial/core/server/McpReadToolsTest.java
       (:mcp:test 41→54, :server:test 1041→1045, 0 failures total)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ools

Adds three MCP write tools alongside M.1.0 reads. Per-controller routing:
ConfigResourceController types use POST/PUT split; ResourceController types
(applications, toolsets, prompts, conversations) use PUT + If-None-Match:*
(create) / If-Match:* (update) so Core's standard etag idiom recovers the
create/update split for PUT-upsert types. validate_only routes to
/v1/admin/validate via single-entity manifest envelope. confirm:true MCP-side
gate before any HTTP. files/settings deferred to M.2.1 with structured
remediation. McpTestSupport extracts handshake helpers from McpReadToolsTest.

Design anchors: 09 §6.1 (tools 4-6), §6.5, §6.6, §7.4
Tests: mcp/.../tools/{Create,Update,Delete}ResourceToolTest.java;
       server/.../McpWriteToolsTest.java (16 cases, incl. §6.2 alias-resolution)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates §5.6 row M.2.0: status 📋 → ✅; commit 313b351.

Captures the locked design choices: Option C validate_only routing
through /v1/admin/validate (Core has no per-resource validate_only
query param); etag-idiom create/update split for ResourceController
PUT-upsert types via If-None-Match:* / If-Match:*; request-side D2
disambiguation in EtagIdiom enum; per-controller routing predicate
on ResourceId; reviewer pre-merge §6.2 alias-resolution fix in
shapeValidate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

# Conflicts:
#	docs/sandbox/dial-unified-config/IMPLEMENTATION.md
dial_update_resource and dial_delete_resource now handle the singleton
'settings' (PUT-upsert / DELETE clears API blob via 3S.2-settings) and
files DELETE (plain /v1/files/{bucket}/{path}, not the M.1.1 metadata
route). Create-side rejections for both types remain - settings has no
POST surface; files binary upload lives in M.3.0. Added
ResourceId.toMutationCorePath so writes skip the metadata-GET prefix.

Design anchors: 09 §6.1 (tools 4-6), §6.5; IMPLEMENTATION.md §5.6 M.2.1
Tests: server/src/test/.../McpWriteToolsTest.java, mcp/src/test/.../ResourceIdTest.java

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M.3.0 (file tools) was implemented to completion on its sub-branch but the
new tool registrations made a pre-existing /mcp deploy-order race
near-deterministic — Core binds the HTTP listener and only later deploys
McpVerticle, so a fast inbound POST /mcp lands while McpRequestHandler is
still returning the M.0-pre 503 stub. Per /dial-mvp §4.1 halt #4, the fix is
carved into sibling slice M.3.1-handshake-readiness; M.3.0 row flipped
to 🚧 with a deps update and a paused-status note.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e ready

Wires a deployment-ready Future<Void> from vertx.deployVerticle(...) through
to McpRequestHandler; dispatch waits up to 2s for the latch then 503s on
timeout. Pauses the request body across the wait so HC5 doesn't read-time-out
after the bodyHandler is installed. Fixes the pre-existing /mcp deploy-order
race that flaked :server:test runs since M.1.0 and went near-deterministic
in M.3.0.

Design anchors: 09 §7.1, §7.2
Tests: mcp/src/test/.../McpRequestHandlerTest.java (4 cases pinning fast/slow
paths + body-survival)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the implementation-discovery note (HC5 180s read-timeout from body
drop; pause/resume contract is load-bearing) plus the 4-case test layout
to the §5.6 row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the two file tools: base64 content + opt-in source_url with
default-deny SSRF guard (CIDR blocklist + allow-list + per-request
timeout + Content-Length pre-check); image-content block on download
for image/* MIME. DialClient gains binary + multipart overloads.
CIDR parsing consolidated into IpAddressRange.parseCidr (used by both
the client-IP allow-list deserializer and the new SSRF guard).
DNS rebinding TOCTOU documented as a known v1 limitation —
sourceUrl is opt-in default-deny.

Design anchors: 09 §6.1 (tools 7-8), §6.7-§6.8, §7.1
Tests: mcp/src/test/.../SourceUrlGuardTest.java (13 cases),
       server/src/test/.../McpFileToolsTest.java (12 cases)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps a single-resource ADD manifest and POSTs to /v1/ops/publication/create
for the Resource Operations API (PENDING -> admin approval). Tool composes
sourceUrl from resolved bucket + name and targetUrl from <type>/<target>+leaf
per spec §3.2 example. Preflight validation rejects malformed target before
the network round-trip. ResourceId.leafName() consolidated from UploadFileTool.

Design anchors: 09 §6.1 (tool 9), §3.2 illustrative composition
Tests: server/src/test/java/com/epam/aidial/core/server/McpPublishToolTest.java

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flips §5.6 register row to ✅ at 12ba627 with five locked decisions
(typed-JsonSchema cap on cross-cutting affordances, preflight as cheap
client-side error, client-computed targetUrl per spec §3.2, Core-side
type validation, ResourceId.leafName extraction). REVIEW pass closures:
two test-coverage gaps (multi-level nested name, non-private alias)
filled with `publishMultiLevelNamePlacesLeafAtTargetRoot` +
`publishWithExplicitBucketSkipsPrivateAlias`.

Design anchors: 09 §6.1 (tool 9), §3.2 illustrative composition
Tests: no new tests (doc-only)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant