Doctrine MCP Bundle exposes an explicitly allow-listed Doctrine ORM model and application-defined
capabilities through the Model Context Protocol. It is an
independent Symfony bundle built on the official mcp/sdk Streamable HTTP transport and does not
require symfony/mcp-bundle.
The bundle is intended for authenticated, multi-user Symfony applications. Symfony Security identifies the caller, mandatory query scopes constrain every Doctrine query, and PHP attributes define the public contract. Doctrine mapping by itself never exposes an entity or field.
dryRun: true by default;QueryBuilder
scopes;UserInterface.composer require errogaht/doctrine-mcp-bundle:^0.1
If Symfony Flex does not register the bundle automatically, enable it manually:
<?php
// config/bundles.php
return [
// ...
Errogaht\DoctrineMcpBundle\DoctrineMcpBundle::class => ['all' => true],
];
Load the bundle route:
# config/routes/doctrine_mcp.yaml
doctrine_mcp:
resource: .
type: doctrine_mcp
The default MCP endpoint is /_mcp. Protect it with the host application's firewall and access
rules:
# config/packages/security.yaml
security:
# Keep the application's existing provider and authentication mechanism.
firewalls:
main:
# ...
access_control:
- { path: ^/_mcp$, roles: ROLE_USER }
The bundle does not issue access tokens and does not implement OAuth. Session cookies, JWT, OAuth
access tokens, service credentials, or another authentication mechanism may be used as long as the
Symfony firewall resolves an authenticated UserInterface for the request.
Create the package configuration:
# config/packages/doctrine_mcp.yaml
doctrine_mcp:
server:
name: "Acme application"
version: "1.0.0"
description: "Authenticated access to selected Acme application data."
http:
path: /_mcp
allowed_hosts: ["app.example.com"]
session:
cache_pool: cache.app
ttl: 3600
query:
default_limit: 25
max_limit: 100
[!IMPORTANT] Configure every public hostname in
allowed_hosts. Settingallowed_hosts: falsedisables DNS-rebinding protection and should only be used behind a trusted proxy that performs an equivalent check.
Apply McpEntity to the class and McpField only to properties that belong to the public MCP
contract:
<?php
namespace App\Entity;
use App\Mcp\Scope\OrderScope;
use Doctrine\ORM\Mapping as ORM;
use Errogaht\DoctrineMcpBundle\Attribute\FilterOperator;
use Errogaht\DoctrineMcpBundle\Attribute\McpEntity;
use Errogaht\DoctrineMcpBundle\Attribute\McpField;
use Errogaht\DoctrineMcpBundle\Attribute\McpOperation;
#[ORM\Entity]
#[McpEntity(
name: 'Order',
description: 'An order owned by the current business.',
operations: [McpOperation::Read, McpOperation::Update],
scopes: [OrderScope::class],
permissions: ['update' => 'ORDER_EDIT'],
)]
final class Order
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[McpField(description: 'The order identifier.')]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[McpField(
description: 'The delivery address while the order is editable.',
updatable: true,
operators: [FilterOperator::Eq, FilterOperator::Contains],
sortable: true,
)]
private string $deliveryAddress = '';
#[ORM\Column(length: 32)]
#[McpField(description: 'The order workflow status.')]
private string $status = 'new';
#[ORM\Column]
#[McpField(readable: false, serverControlled: true, sensitive: true)]
private int $businessId;
// Getters and setters are omitted from this documentation example.
}
Read is the only operation enabled by default. Create, update, and delete must be enabled on the
entity, and individual fields must separately opt in to creatable or updatable.
Every exposed entity must have at least one scope. A scope adds actor-specific predicates to Doctrine queries and owns server-controlled creation data:
<?php
namespace App\Mcp\Scope;
use App\Entity\Order;
use Doctrine\ORM\QueryBuilder;
use Errogaht\DoctrineMcpBundle\Attribute\McpOperation;
use Errogaht\DoctrineMcpBundle\Metadata\EntityDefinition;
use Errogaht\DoctrineMcpBundle\Scope\EntityScopeInterface;
use Errogaht\DoctrineMcpBundle\Security\ActorContext;
/** Restricts orders to the actor's business and enforces editable workflow states. */
final class OrderScope implements EntityScopeInterface
{
public function apply(
QueryBuilder $query,
string $alias,
EntityDefinition $entity,
McpOperation $operation,
ActorContext $actor,
): void {
$businessId = $actor->user->getBusiness()->getId();
$query
->andWhere($alias.'.businessId = :mcp_business_id')
->setParameter('mcp_business_id', $businessId);
if (McpOperation::Update === $operation) {
$query
->andWhere($alias.'.status = :mcp_editable_status')
->setParameter('mcp_editable_status', 'new');
}
}
public function initialize(object $entity, EntityDefinition $definition, ActorContext $actor): void
{
if ($entity instanceof Order) {
// Tenant ownership is controlled by the server, never by MCP input.
$entity->setBusinessId($actor->user->getBusiness()->getId());
}
}
public function assert(
object $entity,
EntityDefinition $definition,
McpOperation $operation,
ActorContext $actor,
): void {
if ($entity instanceof Order && $entity->getBusinessId() !== $actor->user->getBusiness()->getId()) {
throw new \DomainException('Cross-business mutation rejected.');
}
}
}
With standard Symfony service defaults, the scope is autowired, autoconfigured, and tagged automatically:
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: "../src/"
Run the validation command before exposing the endpoint:
php bin/console doctrine-mcp:validate
php bin/console doctrine-mcp:debug Order
Connect an MCP client to https://app.example.com/_mcp and provide the authentication headers or
cookies required by the application's firewall. MCP session negotiation and the Mcp-Session-Id
header are handled by compatible clients automatically.
The public contract is an allow-list assembled from both Doctrine metadata and MCP attributes:
#[McpEntity];#[McpField];Unannotated Doctrine entities and properties are invisible. The bundle never infers exposure from ORM mapping, serializer groups, forms, or API Platform metadata.
McpEntityMcpEntity defines the public entity name and its operation-level policy:
| Option | Type | Default | Description |
|---|---|---|---|
name |
?string |
Doctrine class short name | Stable public name used by all generic tools. |
description |
?string |
null |
Description shown to MCP clients and agents. |
operations |
list<McpOperation> |
[Read] |
Allowed CRUD operations. |
scopes |
list<class-string> |
[] |
Entity-specific EntityScopeInterface services. At least one entity or global scope is required. |
permissions |
array<string, string> |
[] |
Symfony voter attributes keyed by read, create, update, or delete. |
handler |
?string |
null |
Service ID of a custom EntityMutationHandlerInterface. |
Entity names must be unique across all Doctrine entity managers.
McpFieldMcpField defines a property's public representation and field-level policy:
| Option | Type | Default | Description |
|---|---|---|---|
name |
?string |
Property name | Public field name. |
description |
?string |
null |
Human-readable meaning and usage constraints. |
readable |
bool |
true |
Include the field in MCP output. |
creatable |
bool |
false |
Accept the field during create. |
updatable |
bool |
false |
Accept the field during update. |
filterable |
bool |
true |
Allow filtering for scalar fields. |
operators |
list<FilterOperator> |
[Eq] |
Explicitly allowed filter operators. |
sortable |
bool |
false |
Allow ordering by the field. |
sensitive |
bool |
false |
Replace serialized and audited values with [REDACTED]. |
serverControlled |
bool |
false |
Reject the field in create and update input even if a write flag was set accidentally. |
format |
?string |
null |
Format hint returned by doctrine_describe_entity. |
reference |
?string |
null |
Name of a ReferenceProviderInterface available through doctrine_reference_values. |
[!NOTE]
formatis descriptive metadata for the agent. Use PHP types, Doctrine types, Symfony Validator constraints, or a custom mutation handler for server-side validation.
The built-in codec safely converts common Doctrine and PHP values:
| Value | MCP representation |
|---|---|
Strings, integers, floats, booleans, null |
Native JSON value |
| Doctrine integer types | JSON integer |
| Doctrine float and decimal types | JSON number |
| Doctrine boolean | JSON boolean |
| Date and datetime types | ISO-8601 string |
Symfony Uuid |
RFC 4122 string |
| Backed enum | Backing value |
| Unit enum | Case name |
| Doctrine JSON | JSON object or array |
| Association | { "entity": "EntityName", "id": { ... } } |
Unknown object graphs are rejected instead of being serialized recursively.
The bundle exposes a fixed tool surface. Adding more entities does not create one tool per entity:
| Tool | Purpose |
|---|---|
doctrine_list_entities |
List entities and operations available to the current actor. |
doctrine_describe_entity |
Describe identifiers, fields, formats, filters, write flags, and references. |
doctrine_search |
Search scoped records with allow-listed filters, sorting, limits, and cursor pagination. |
doctrine_get |
Read one scoped record by its complete identifier object. |
doctrine_create |
Preview or create an entity. |
doctrine_update |
Preview or update writable fields on a scoped entity. |
doctrine_delete |
Preview or delete a scoped entity. |
doctrine_reference_values |
Read actor-aware allowed-value suggestions from a reference provider. |
All successful results contain ok, operation, and traceId.
Identifiers are always objects, including single-column identifiers:
{
"entity": "Order",
"id": { "id": 42 }
}
Composite identifiers must contain every identifier field and no additional fields.
Equality can use the short form:
{
"entity": "Order",
"filters": {
"status": "new"
}
}
Other operators use an explicit specification:
{
"entity": "Order",
"filters": {
"deliveryAddress": { "op": "contains", "value": "Main Street" },
"createdAt": { "op": "gte", "value": "2026-01-01T00:00:00+00:00" },
"status": { "op": "in", "value": ["new", "confirmed"] }
},
"sort": { "field": "createdAt", "direction": "DESC" },
"limit": 25,
"cursor": null
}
Supported operators are eq, in, contains, gt, gte, lt, and lte. An operator works only
when it is listed in that field's McpField.operators. Associations cannot be filtered through the
generic tool.
Search returns items and an opaque nextCursor. It deliberately does not execute a separate
total-count query. Without an explicit sort, results are ordered by Doctrine identifier fields.
Create, update, and delete default to dryRun: true:
{
"entity": "Order",
"id": { "id": 42 },
"changes": {
"deliveryAddress": "10 Main Street"
},
"dryRun": true
}
A successful preview returns sanitized before and after values, but does not flush the standard
Doctrine mutation. The caller must review the preview and explicitly send dryRun: false to apply
it.
Symfony Validator constraints run for both previews and applied create/update operations. Mutation attempts also produce audit records.
Associations are represented as shallow references; the bundle never serializes an unbounded Doctrine object graph:
{
"entity": "Customer",
"id": { "id": 7 }
}
For a writable to-one association, send either an identifier object or a scalar for a single-column identifier:
{
"customer": { "id": 7 }
}
The target entity must also be exposed with McpEntity. It is resolved using the target entity's
read scopes and voter, so a caller cannot attach an inaccessible record.
Writable collections use explicit add/remove change sets:
{
"tags": {
"add": [{ "id": 3 }, { "id": 5 }],
"remove": [{ "id": 2 }]
}
}
The entity must provide conventional addTag() and removeTag() methods, or use a custom mutation
handler. The number of association changes is bounded by query.max_association_items.
EntityScopeInterface is the primary data-isolation boundary:
apply() adds bound predicates to search and identifier queries;initialize() fills tenant ownership and other server-controlled values during create;assert() verifies state and ownership before a mutation is accepted.Scopes are applied to search, get, update, delete, and writable association resolution. The bundle
does not use EntityManager::find() for actor-addressable records.
Use bound parameters in every scope. Scope logic may add joins and predicates for ownership, organization membership, soft deletion, geographic access, or workflow state.
Global scopes apply to every exposed entity:
# config/packages/doctrine_mcp.yaml
doctrine_mcp:
global_scopes:
- App\Mcp\Scope\ApplicationStatusScope
If an application intentionally exposes unscoped records, opt in explicitly:
use Errogaht\DoctrineMcpBundle\Scope\UnrestrictedScope;
#[McpEntity(scopes: [UnrestrictedScope::class])]
final class PublicCountry
{
// ...
}
[!WARNING]
UnrestrictedScopeis a security-review marker, not a convenience default. Never use it for tenant-owned or user-owned data.
If Symfony autoconfiguration is disabled, tag scope services manually with doctrine_mcp.scope.
Scopes decide which rows are addressable. Voters decide whether the current actor may perform an operation:
#[McpEntity(
operations: [McpOperation::Read, McpOperation::Update, McpOperation::Delete],
scopes: [OrderScope::class],
permissions: [
'read' => 'ORDER_VIEW',
'update' => 'ORDER_EDIT',
'delete' => 'ORDER_DELETE',
],
)]
final class Order
{
// ...
}
Discovery and collection checks authorize against the entity class name. Record-specific get, update, and delete checks authorize against the resolved entity object. Create is authorized against the initialized entity object.
Scopes remain mandatory when voters are configured. A voter is not a replacement for a query predicate because filtering unauthorized rows after loading them can leak data and identifiers.
The default actor provider reads the authenticated user from Symfony Security and creates an
ActorContext containing the user identifier, original UserInterface, and roles.
Implement ActorProviderInterface to add tenant identifiers or other scalar security attributes:
<?php
namespace App\Mcp;
use Errogaht\DoctrineMcpBundle\Security\ActorContext;
use Errogaht\DoctrineMcpBundle\Security\ActorProviderInterface;
use Errogaht\DoctrineMcpBundle\Security\UnauthenticatedActorException;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\User\UserInterface;
/** Maps the authenticated application user into the stable MCP security context. */
final class ActorProvider implements ActorProviderInterface
{
public function __construct(private readonly Security $security)
{
}
public function current(): ActorContext
{
$user = $this->security->getUser();
if (!$user instanceof UserInterface) {
throw new UnauthenticatedActorException('Authentication is required.');
}
return new ActorContext(
id: $user->getUserIdentifier(),
user: $user,
roles: array_values($user->getRoles()),
attributes: ['business_id' => $user->getBusiness()->getId()],
);
}
}
Configure its service ID:
# config/packages/doctrine_mcp.yaml
doctrine_mcp:
actor_provider: App\Mcp\ActorProvider
Actor attributes must contain only scalar values or null. Scopes and custom handlers may inspect
both ActorContext.attributes and the original ActorContext.user.
Reference providers publish bounded, searchable, actor-aware value lists. They are useful for time zones, statuses, countries, warehouses, or project-specific dictionaries:
<?php
namespace App\Mcp\Reference;
use Errogaht\DoctrineMcpBundle\Reference\ReferencePage;
use Errogaht\DoctrineMcpBundle\Reference\ReferenceProviderInterface;
use Errogaht\DoctrineMcpBundle\Security\ActorContext;
/** Provides agent-readable time-zone choices without exposing an internal table. */
final class TimezoneReferenceProvider implements ReferenceProviderInterface
{
public function name(): string
{
return 'timezones';
}
public function values(ActorContext $actor, ?string $query, int $limit, ?string $cursor): ReferencePage
{
$values = array_filter(
\DateTimeZone::listIdentifiers(),
static fn (string $timezone): bool => null === $query || str_contains(strtolower($timezone), strtolower($query)),
);
$items = array_map(
static fn (string $timezone): array => ['value' => $timezone, 'label' => $timezone],
array_slice(array_values($values), 0, $limit),
);
return new ReferencePage($items);
}
}
Attach the provider name to a field:
#[McpField(
description: 'IANA time-zone identifier.',
updatable: true,
format: 'iana-timezone',
reference: 'timezones',
)]
private string $timezone = 'UTC';
The agent calls doctrine_reference_values with reference, optional query, limit, and
cursor. Providers are tagged automatically; without autoconfiguration, use
doctrine_mcp.reference_provider.
[!NOTE] A reference provider documents and suggests values. Enforce membership with Symfony Validator constraints or domain logic when the field is written.
The default mutation pipeline uses a no-argument constructor, Symfony PropertyAccess setters,
Doctrine persistence, and conventional collection methods. Use EntityMutationHandlerInterface for
aggregate factories, value objects, workflow transitions, or domain services:
<?php
namespace App\Mcp\Mutation;
use App\Entity\Order;
use App\Service\OrderManager;
use Errogaht\DoctrineMcpBundle\Metadata\EntityDefinition;
use Errogaht\DoctrineMcpBundle\Mutation\EntityMutationHandlerInterface;
use Errogaht\DoctrineMcpBundle\Security\ActorContext;
/** Routes MCP mutations through the same domain service used by the application. */
final class OrderMutationHandler implements EntityMutationHandlerInterface
{
public function __construct(private readonly OrderManager $orders)
{
}
public function create(EntityDefinition $definition, array $values, ActorContext $actor, bool $dryRun): object
{
return $this->orders->createForMcp($values, $actor->user, $dryRun);
}
public function update(object $entity, EntityDefinition $definition, array $changes, ActorContext $actor, bool $dryRun): object
{
if (!$entity instanceof Order) {
throw new \LogicException('Unexpected MCP entity.');
}
return $this->orders->updateForMcp($entity, $changes, $actor->user, $dryRun);
}
public function delete(object $entity, EntityDefinition $definition, ActorContext $actor, bool $dryRun): void
{
$this->orders->deleteForMcp($entity, $actor->user, $dryRun);
}
}
Reference the handler by service ID:
#[McpEntity(
operations: [McpOperation::Read, McpOperation::Create, McpOperation::Update],
scopes: [OrderScope::class],
handler: OrderMutationHandler::class,
)]
final class Order
{
// ...
}
Custom handlers receive the dryRun flag and own their persistence and transaction behavior. They
must not persist during a preview. Entity scopes, voters, and Symfony validation still run around
handler results. Without autoconfiguration, tag handlers with doctrine_mcp.mutation_handler.
The same endpoint can expose application-specific tools, resources, resource templates, and prompts.
Put autowired services in src/Mcp and use attributes from the official PHP MCP SDK.
<?php
namespace App\Mcp;
use App\Service\OrderReportService;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\ToolAnnotations;
/** Provides an aggregate report that does not map to generic entity CRUD. */
final class OrderReportTools
{
public function __construct(private readonly OrderReportService $reports)
{
}
/** @return array{orders: int, revenue: float} */
#[McpTool(
name: 'orders_daily_summary',
description: 'Return the current actor\'s order summary for an ISO-8601 date.',
annotations: new ToolAnnotations(readOnlyHint: true, idempotentHint: true),
)]
public function dailySummary(string $date): array
{
return $this->reports->dailySummary(new \DateTimeImmutable($date));
}
}
The SDK derives the tool input schema from PHP parameter types and documentation. Constructor dependencies are resolved by the Symfony container.
<?php
namespace App\Mcp;
use Mcp\Capability\Attribute\McpPrompt;
use Mcp\Capability\Attribute\McpResource;
use Mcp\Capability\Attribute\McpResourceTemplate;
/** Publishes application guidance through standard MCP capabilities. */
final class ApplicationCapabilities
{
#[McpResource(
uri: 'docs://orders/policy',
name: 'order_policy',
description: 'Current order editing policy.',
mimeType: 'text/markdown',
)]
public function orderPolicy(): string
{
return 'Orders can be edited before they enter delivery.';
}
#[McpResourceTemplate(
uriTemplate: 'docs://orders/{locale}/policy',
name: 'localized_order_policy',
description: 'Localized order editing policy.',
mimeType: 'text/markdown',
)]
public function localizedOrderPolicy(string $locale): string
{
return sprintf('Order policy for locale %s.', $locale);
}
#[McpPrompt(
name: 'prepare_order_summary',
description: 'Prepare a concise customer-facing order summary.',
)]
public function prepareOrderSummary(string $orderId): string
{
return sprintf('Summarize order %s using only data available to the current actor.', $orderId);
}
}
Change discovery directories when capabilities live elsewhere:
# config/packages/doctrine_mcp.yaml
doctrine_mcp:
discovery:
scan_dirs:
- src/Mcp
- src/Integration/Mcp
exclude_dirs:
- src/Mcp/Internal
Paths are relative to kernel.project_dir. The classes must also be registered as Symfony services.
With non-standard service loading, register the namespace explicitly:
# config/services.yaml
services:
App\Mcp\:
resource: "../src/Mcp/"
autowire: true
autoconfigure: true
[!IMPORTANT] Custom capabilities share the authenticated transport and actor-bound MCP session, but they do not automatically pass through Doctrine CRUD scopes,
dryRun, field permissions, voters, or mutation auditing. Inject application services, Symfony Security, orActorProviderInterfaceand enforce the capability's own authorization and domain rules.
The controller resolves the actor before MCP payload processing. Unauthenticated requests receive
HTTP 401.
Streamable HTTP sessions are stored in the configured cache pool. Every session ID is bound to a
SHA-256 fingerprint of the actor identifier; replaying another user's session ID returns HTTP 403.
Session IDs are hashed in logs.
For multi-instance deployments, use a shared cache pool such as Redis:
# config/packages/cache.yaml
framework:
cache:
pools:
cache.doctrine_mcp_sessions:
adapter: cache.adapter.redis
# config/packages/doctrine_mcp.yaml
doctrine_mcp:
http:
session:
cache_pool: cache.doctrine_mcp_sessions
ttl: 3600
The actor identifier returned by ActorProviderInterface must be stable and unique across all users
or service identities that can access the endpoint.
Every request has a traceId. The endpoint accepts X-Request-Id; otherwise it generates one and
returns it in the response header.
The doctrine_mcp log channel emits structured events including:
dryRun, and trace identifier;Secret-like keys such as passwords, tokens, authorization headers, API keys, and cookies are
redacted. Fields marked sensitive: true are redacted during entity serialization and mutation
auditing.
The default audit sink writes sanitized mutation records to the logger. Implement
AuditSinkInterface for an immutable database table or external audit service:
<?php
namespace App\Mcp\Audit;
use Errogaht\DoctrineMcpBundle\Audit\AuditSinkInterface;
/** Persists sanitized MCP mutation records in the application's audit store. */
final class DoctrineAuditSink implements AuditSinkInterface
{
public function record(array $record): void
{
// Persist the already-sanitized record in an append-only store.
}
}
# config/packages/doctrine_mcp.yaml
doctrine_mcp:
audit_sink: App\Mcp\Audit\DoctrineAuditSink
# config/packages/doctrine_mcp.yaml
doctrine_mcp:
server:
name: "Doctrine MCP"
version: "0.1.1"
description: "Secure Doctrine ORM capabilities exposed over MCP."
instructions: >-
Discover entity capabilities before reading or mutating data. All results are constrained to
the authenticated actor.
http:
path: /_mcp
allowed_hosts: null
max_body_bytes: 4194304
session:
cache_pool: cache.app
prefix: "doctrine-mcp-"
ttl: 3600
discovery:
scan_dirs: ["src/Mcp"]
exclude_dirs: []
query:
default_limit: 25
max_limit: 100
max_association_items: 100
actor_provider: doctrine_mcp.actor_provider.security
audit_sink: Errogaht\DoctrineMcpBundle\Audit\LoggerAuditSink
global_scopes: []
| Option | Default | Description |
|---|---|---|
server.name |
Doctrine MCP |
MCP server display name. |
server.version |
0.1.1 |
Application-facing MCP server version. |
server.description |
See example | Server description returned during discovery. |
server.instructions |
See example | Instructions provided to MCP clients. |
http.path |
/_mcp |
Streamable HTTP endpoint path. |
http.allowed_hosts |
null |
SDK localhost defaults; use a hostname list in production or false to disable the middleware. |
http.max_body_bytes |
4194304 |
Maximum MCP request body size; minimum 1024 bytes. |
http.session.cache_pool |
cache.app |
Symfony PSR-6 cache pool used for MCP state and actor binding. |
http.session.prefix |
doctrine-mcp- |
Cache key prefix. |
http.session.ttl |
3600 |
Session lifetime in seconds; minimum 60. |
discovery.scan_dirs |
['src/Mcp'] |
Capability discovery paths relative to the project directory. |
discovery.exclude_dirs |
[] |
Paths excluded from discovery. |
query.default_limit |
25 |
Default search page size. |
query.max_limit |
100 |
Maximum search and SDK pagination size. |
query.max_association_items |
100 |
Maximum add/remove items in one collection mutation. |
actor_provider |
Symfony Security provider | Service implementing ActorProviderInterface. |
audit_sink |
Logger sink | Service implementing AuditSinkInterface. |
global_scopes |
[] |
Scope classes applied to every exposed entity. |
Validate all exposed entities, mapped fields, scope registrations, server-controlled write flags, and reference configuration:
php bin/console doctrine-mcp:validate
Inspect the normalized public contract of one entity:
php bin/console doctrine-mcp:debug Order
Run validation during deployment and after changing Doctrine metadata, MCP attributes, scope services, or reference providers. Clear the Symfony cache when a production deployment changes discovered attributes or container configuration.
Before connecting an agent:
access_control;allowed_hosts;UnrestrictedScope;dryRun;doctrine-mcp:validate and inspect each entity with doctrine-mcp:debug;The default actor provider requires an authenticated Symfony UserInterface. Check the firewall
pattern, authenticator, access token or cookie, and access_control rules for the MCP path.
Confirm that Doctrine manages the class, the class has McpEntity, its public name is unique, and
the application cache was cleared after deployment.
Add an EntityScopeInterface class to McpEntity.scopes, configure a global scope, or deliberately
use UnrestrictedScope for reviewed public data.
Ensure Symfony autoconfiguration covers the class. Otherwise add doctrine_mcp.scope,
doctrine_mcp.reference_provider, or doctrine_mcp.mutation_handler manually.
Check that its file is inside discovery.scan_dirs, outside exclude_dirs, has an SDK MCP
attribute, and is registered as an autoconfigured Symfony service. Clear the Symfony cache
afterward.
The entity operation and field write flag must both be enabled. Identifiers, unannotated fields,
read-only fields, and serverControlled fields are rejected by design.
Expose the target entity, provide its exact identifier shape, and ensure the current actor can read it through the target's scopes and voter.
Install dependencies and run the complete package checks:
composer install
composer check
Doctrine integration tests require pdo_sqlite. The CI matrix covers PHP 8.1 with the lowest
supported Symfony and Doctrine versions and stable dependencies on newer PHP versions.
Every successful CI run for a push to master publishes the tested commit automatically. The
release workflow increments the patch version, creates an annotated Git tag and GitHub Release, and
updates Packagist immediately. The Packagist webhook remains enabled as a second synchronization
path.
Use the manual dispatcher only for an intentional minor or major release, or to retry automation:
scripts/release.sh minor
scripts/release.sh major
To ask Packagist to recrawl the existing release without creating a version, run:
scripts/update-packagist.sh
Release credentials are stored as GitHub Actions Secrets. Local recovery uses a machine-bound
encrypted systemd-creds credential; secrets are never committed to the repository.
This project is proprietary. See LICENSE. Adapted MIT-licensed transport code is documented in THIRD_PARTY_NOTICES.md.
How can I help you explore Laravel packages today?