You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- Remove nil conflict resolver path; always use default prefix strategy
- Filter decorator evaluates directly on sess.Tools(), no advertisedSetProvider
- WithConflictResolver always required in factory options
- Move processBackendTools and actualBackendCapabilityName to session package
- Expand backward compatibility table with all config fields
- Remove moot open question about filter name matching
- Rewrite testing strategy as feature combination matrix covering
filter, renames, conflict resolution, composite tools, optimizer,
and authn interactions across 11 E2E test scenarios
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When `conflictResolver` or `toolConfigMap` is non-nil:
78
+
A conflict resolver is always provided — `NewConflictResolver(nil)` defaults to prefix strategy with `{workload}_` format, matching current behavior. The pipeline:
79
79
80
80
1. Group tools by `BackendID`
81
-
2. Apply `aggregator.ProcessBackendTools()` per backend (overrides)
81
+
2. Apply `processBackendTools()` per backend (overrides)
4. Build routing table keyed by resolved names, with `OriginalCapabilityName` set via `aggregator.ActualBackendCapabilityName()`
83
+
4. Build routing table keyed by resolved names, with `OriginalCapabilityName` set via `actualBackendCapabilityName()`
84
84
5. Build `allTools` from ALL resolved tools (no filtering)
85
-
6. Compute `advertisedSet map[string]bool` keyed by resolved name — evaluates the filter logic inline
86
85
87
-
When both are nil: current first-writer-wins logic, `advertisedSet = nil`.
88
-
89
-
The `advertisedSet` is stored on `defaultMultiSession` as a private field.
86
+
The old `buildRoutingTable` (first-writer-wins, no conflict resolution) is removed entirely — the conflict resolver always handles name conflicts.
90
87
91
88
#### Filter decorator
92
89
93
90
`pkg/vmcp/session/filterdec/decorator.go` (new)
94
91
92
+
The filter decorator evaluates the advertising filter directly on `sess.Tools()` using the tool config. Each tool in the session has `BackendID` set, which is sufficient to evaluate per-workload `excludeAll` and `filter` rules. The filter config references post-override tool names, which match `tool.Name` after conflict resolution when no prefix was applied, or can be matched by stripping the known prefix.
Insert filter decorator between composite tools and optimizer:
120
+
Insert filter decorator between composite tools and optimizer. The filter decorator is always applied — it receives the tool config and evaluates advertising decisions against `sess.Tools()` at decoration time:
**Moved to session package** (implementation detail, not exported):
157
+
-`processBackendTools` — moves from `aggregator` to `session` package
158
+
-`actualBackendCapabilityName` — moves from `aggregator` to `session` package
167
159
168
160
### Configuration Changes
169
161
170
162
None. All existing configuration fields (`aggregation.excludeAllTools`, `aggregation.tools[].excludeAll`, `aggregation.tools[].filter`, `aggregation.tools[].overrides`) continue to work identically. The implementation moves from the aggregator to the factory + decorator.
171
163
172
164
### Data Model Changes
173
165
174
-
`defaultMultiSession` gains a private `advertisedSet map[string]bool` field. Not exposed on any public interface.
166
+
None. The filter decorator holds the tool config directly and evaluates advertising decisions at decoration time against `sess.Tools()`.
175
167
176
168
## Security Considerations
177
169
@@ -233,7 +225,17 @@ The `advertisedSet` is computed once at session creation and is immutable. The f
233
225
234
226
### Backward Compatibility
235
227
236
-
Fully backward compatible. All configuration fields work identically. Client-visible `tools/list` output is unchanged. The `Aggregator` interface is removed but it's an internal interface — no external consumers.
228
+
All configuration fields work identically. Client-visible behavior is unchanged. The `Aggregator` interface is removed but it's internal — no external consumers.
229
+
230
+
**Feature-by-feature analysis of the new decorator ordering:**
231
+
232
+
| Feature | Current flow | New flow | Impact |
233
+
|---|---|---|---|
234
+
|**Authentication (Cedar)**| HTTP middleware intercepts `tools/list` and `tools/call` at the JSON-RPC level, evaluates Cedar policies against resolved tool names | Unchanged — Cedar middleware sits above the entire session decorator stack, sees the same resolved tool names in requests/responses | None |
235
+
|**Composite tool calling**| Workflow engine receives `sess.Tools()` (advertised only) and `sess.GetRoutingTable()`. Calls `backendClient.CallTool()` directly (bypasses session decorators). `getToolInputSchema()` fails for non-advertised tools | Workflow engine receives `sess.Tools()` (ALL tools, since composite tools decorator sits below the filter). `backendClient.CallTool()` still bypasses decorators. `getToolInputSchema()` now finds schemas for all tools |**Bug fix** — coercion works for non-advertised tools |
236
+
|**Advertising filter**| Aggregator applies `shouldAdvertiseTool` before session creation. `sess.Tools()` returns only advertised tools | Filter decorator applies after composite tools. `sess.Tools()` at the filter level returns only advertised tools + composite tools. Below the filter (where composite tools operate), `sess.Tools()` returns all tools | Same client-visible result; composite tools see more |
237
+
|**Optimizer**| Wraps session after composite tools, indexes `sess.Tools()` (advertised + composite) into find_tool/call_tool | Wraps session after filter, indexes `sess.Tools()` (advertised + composite) — same tools as before | None |
238
+
|**Tool call routing**|`defaultMultiSession.CallTool` looks up routing table → `GetBackendCapabilityName()` → backend HTTP call | Unchanged — routing table is built with the same resolved names and `OriginalCapabilityName` values, just by the factory instead of the aggregator | None |
237
239
238
240
### Forward Compatibility
239
241
@@ -247,29 +249,137 @@ The decorator-based architecture makes it straightforward to add new session-lev
247
249
248
250
### Phase 1: Core changes
249
251
250
-
1.Inline aggregator pipeline into `buildRoutingTable` in `pkg/vmcp/session/factory.go`
251
-
2.Export `ProcessBackendTools` and `ActualBackendCapabilityName` from aggregator package
252
+
1.Move `processBackendTools` and `actualBackendCapabilityName` from aggregator to session package
253
+
2.Inline aggregator pipeline into `buildRoutingTable` in `pkg/vmcp/session/factory.go` — always use conflict resolver
252
254
3. Create filter decorator at `pkg/vmcp/session/filterdec/decorator.go`
253
255
4. Wire filter decorator in `pkg/vmcp/server/sessionmanager/factory.go`
254
256
5. Update server wiring in `cmd/vmcp/app/commands.go` and `pkg/vmcp/server/server.go`
255
257
256
258
### Phase 2: Cleanup
257
259
258
260
6. Delete `Aggregator` interface, `defaultAggregator`, and associated methods
259
-
7. Delete dead discovery types (`AggregatedCapabilities`, `BackendDiscoverer`, etc.)
260
-
8.Update tests
261
+
7. Delete dead types (`AggregatedCapabilities`, `BackendDiscoverer`, etc.)
262
+
8.Delete aggregator tests for removed methods; add integration tests on MultiSession construction/decoration
-**Existing tests**: Conflict resolver tests remain unchanged. Aggregator tests for deleted methods are removed.
266
+
All validation is at the K8s/Ginkgo E2E level (`test/e2e/thv-operator/virtualmcp/`). Tests deploy real MCPServers + VirtualMCPServers to a Kind cluster and exercise the full stack via MCP clients.
267
+
268
+
### Feature combination matrix
269
+
270
+
The features being modified interact with each other. The matrix below maps every relevant combination to an E2E test — either an existing one that provides coverage or a new one that must be written.
The specific bug this RFC fixes. When a composite tool workflow step calls a hidden backend tool with a numeric parameter, `getToolInputSchema()` fails to find the schema, skips coercion, and the backend rejects `"42"` (string) instead of `float64(42)`.
310
+
311
+
**Setup**:
312
+
- Backend with a tool accepting an integer parameter (requires adding an `add` tool to yardstick: `{"a": integer, "b": integer}` → returns sum)
313
+
-`ExcludeAll: true` for the backend
314
+
- Composite tool whose workflow step calls the hidden tool via template expansion (`"{{ .params.a }}"`)
No existing test exercises overrides and filter together. The `shouldAdvertiseTool` comment bug (says "before overrides" but receives post-override name) was never caught because this combination is untested.
328
+
329
+
**Setup**:
330
+
- Backend with tool `echo`, overridden to `custom_echo`
331
+
- Filter configured with `["echo"]` (pre-override name)
332
+
333
+
**Assertions**:
334
+
- Document whether the filter matches pre- or post-override names
335
+
- Verify the overridden tool is callable by its new name
336
+
- Lock in the actual behavior so the refactor preserves it
Lightweight unit tests for the new filter decorator in isolation:
363
+
-`Tools()` with `excludeAll` → only composite tools returned
364
+
-`Tools()` with `filter` → only matching backend tools + composite tools
365
+
-`CallTool` passes through for both advertised and non-advertised tools
366
+
- No filter config → all tools pass through
367
+
368
+
### Cleanup
369
+
370
+
- Delete aggregator unit tests for removed methods (`ProcessPreQueriedCapabilities`, `MergeCapabilities`, `shouldAdvertiseTool`)
371
+
- Conflict resolver unit tests remain unchanged
372
+
373
+
### Verification
268
374
269
375
```bash
376
+
# Unit tests
270
377
go vet ./pkg/vmcp/... ./cmd/vmcp/...
271
378
go test ./pkg/vmcp/... ./cmd/vmcp/...
272
379
task lint-fix
380
+
381
+
# E2E tests (Kind cluster)
382
+
task test-e2e
273
383
```
274
384
275
385
## Documentation
@@ -279,8 +389,7 @@ task lint-fix
279
389
280
390
## Open Questions
281
391
282
-
1. Should the `shouldAdvertiseTool` comment (incorrectly says "before overrides" but actually receives post-override names) be fixed as part of this work, or is it moot since the method is deleted?
283
-
2. Should we add a Filter+Override combined test before deleting the aggregator, to document the actual behavior?
392
+
None — all design questions resolved during review.
0 commit comments