Problem/Motivation
Currently tool configuration is scattered across multiple entity properties — enabled_tools, locked_tools, and tool_operations — which makes the model harder to reason about, more error-prone and less type-safe. Maintaining parallel arrays introduces duplication, increases the chance of inconsistent state, and complicates validation, serialization, and change-tracking.
Proposed consolidation: move every tool's configuration into a single tools property on the entity where each tool is represented by one complete configuration object/array:
[ 'name' => 'tool_name', 'description' => 'Tool description', 'input_schema' => [...], // MCP tool schema // Configuration 'enabled' => true, 'locked' => false, 'operation' => 'read', // read|write // Locked definition (only if locked === true) 'locked_definition' => [ 'name' => 'tool_name', 'description' => 'Tool description', 'input_schema' => [...], ], ]
Consolidation creates a single source of truth and improves validation, maintainability, and type safety. It also prepares the codebase for properly typed DTOs/value objects.
Steps to reproduce
- Create an entity with the current model using
enabled_tools,locked_toolsandtool_operations. - Perform an operation requiring tool state validation (e.g. checking enabled/locked/operation mode).
- Observe that logic must reconcile multiple arrays, increasing risk of inconsistent state.
- Attempt to add or modify tool metadata; duplication across arrays complicates maintenance.
Proposed resolution
Replace the scattered arrays with a single tools list on the entity and introduce typed DTOs/Value Objects for clarity and type safety:
- ToolCollection — collection wrapper for all tools.
- Tool — represents a single tool (config + metadata).
- InputSchema — representation of the tool’s input schema.
- LockDefinition — metadata snapshot when a tool is locked.
Example DTO API:
// ToolCollection get(string $name): ?Tool add(Tool $tool) remove(string $name) toArray()/fromArray() // Tool getName(), getDescription(), isEnabled(), isLocked(), getOperation() withEnabled(), withLocked(), withOperation(), withLockedDefinition() toArray()/fromArray()
Key behaviours:
- When
locked === true, alocked_definitionmust be present. - Validation occurs in constructors/factories.
- Entity stores
toolsas a structured JSON or equivalent type.
Remaining tasks
- Design and implement DTOs (
Tool,ToolCollection,InputSchema,LockDefinition). - Create a migration script to consolidate old arrays into the new
toolsstructure. - Update entity schema: add
toolsproperty and deprecate old fields. - Refactor logic to use DTO-based API instead of raw arrays.
- Write full validation and serialization tests.
- Update documentation and API references.
- Add getters/setters on the entity for
ToolCollection.
User interface changes
Admin / configuration UI updates:
- Replace multiple tool-related UI fields with a single consolidated tool configuration table.
- Display locked tools with read-only metadata pulled from
locked_definition. - Allow editing of a full tool object via a structured UI.
Migration UI:
- Optional: provide a migration preview showing old vs new tool definitions.
API changes
New entity methods:
getTools(): ToolCollectionsetTools(ToolCollection $tools)getTool(string $name): ?ToolhasTool(string $name): bool
Legacy methods become deprecated but remain readable for one release cycle.
Example JSON shape:
{ "tools": [ { "name": "tool_name", "description": "Tool description", "input_schema": { }, "enabled": true, "locked": false, "operation": "read", "locked_definition": null } ] }
Data model changes
Introduce a single tools field (JSON/document/list-of-maps). Remove the following legacy fields:
enabled_toolslocked_toolstool_operations
Migration strategy:
- Read existing arrays and merge by tool name.
- Derive
enabled,locked, andoperationvalues. - If
locked, generate a properlocked_definition. - Write consolidated structures into the new
toolsfield. - Deprecate old fields and remove after client migration period.
Issue fork mcp_client-3561170
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
robertoperuzzoComment #3
harivansh commentedComment #5
harivansh commentedComment #6
robertoperuzzo@harivansh I just fixed coding standards and unit tests. The DTO classes work properly! Thank you very much.
Comment #7
robertoperuzzoHi @harivansh,
I was wondering why you used the "wither" methods in Tool.php.
So, we know that using this pattern, the benefits are:
$updatedTool = $tool->withEnabled(true); // Returns new Tool, original unchangedBut the drawbacks are:
with*()call creates a complete new object. If you chain multiple changes, you create intermediate objects that immediately become garbage:$tool->withEnabled(true)->withOperation($op)->withLocked(true, $def); // Creates 3 Tool objects, only the last one is usedSo, I would like to understand if we are forced to use
with*methods or use a different approach like this:Builder Pattern
Create a mutable builder that constructs the immutable object only once:
Comment #8
marcus_johansson commentedWorks well, I have tested different setups. Also code wise I did do two comments, but they are NITs and can be changed later.
So ready to merge.
#3562654: Add a disable/enable all tools checkbox and #3537086: Lock title and descriptions are nice to have, but I think we could release an alpha with this and start thinking about error handling we msised.
Comment #9
marcus_johansson commentedThank you all, getting merged.