Skip to the content.

13 — Configuration reference

Every YAML knob, with defaults and recommendations. Place under config/packages/json_rpc_server.yaml.

Zero-config defaults

json_rpc_server: ~

Equivalent to the full tree below. Everything is optional; override only what you need.

Full tree

json_rpc_server:
    # ---------- security ----------
    security:
        roles_match: any
        expose_role_names: true
        default_roles: []         # e.g. ['ROLE_USER'] for secure-by-default
        public_prefixes: []       # e.g. ['public.', 'health.']
        public_methods: []        # e.g. ['ping', 'version']
        prefix_roles: {}          # e.g. {'admin.': ['ROLE_ADMIN'], 'internal.': ['ROLE_INTERNAL']}

    # ---------- request / response shape ----------
    max_request_size: 1048576
    max_json_depth: 32
    http_status:
        enabled: false

    json:
        encode_flags: 96            # JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES

    headers:
        deprecation: 'X-Rpc-Deprecated'

    context:
        request_id_header: 'X-Request-Id'

    # ---------- params / DTOs ----------
    params:
        allow_positional_dto: false
        reject_unknown: true

    serializer:
        datetime_format: iso8601
        date_format: 'Y-m-d'
        timezone: ~

    # ---------- handlers in DI ----------
    handlers:
        public: false
        shared: false

    # ---------- routes ----------
    routes:
        rpc:        { path: /rpc,        enabled: true }
        stream:     { path: /rpc/stream, enabled: true }
        mcp_tools:  { path: /mcp/tools,  enabled: true }
        mcp_call:   { path: /mcp/call,   enabled: true }

    # ---------- caching ----------
    cache:
        default_pool: cache.app
        pools: {}
        max_readable_key_length: 200
        key_prefix: rpc.cache
        hash_prefix: rpc

    # ---------- rate limiter ----------
    rate_limiter:
        cache_pool: cache.app

    # ---------- streaming ----------
    stream:
        headers:
            X-Accel-Buffering: no
            Cache-Control: no-cache

    # ---------- profiler ----------
    profiler:
        enabled: true

    # ---------- MCP ----------
    mcp:
        enabled: false
        format_header: 'X-Mcp-Format'
        format_query: 'format'
        default_format: json
        apply_rate_limit: true
        expose_all: false
        exclude_prefixes: []
        exclude_methods: []
        whitelist_methods: []
        schema_max_depth: 6
        markdown:
            max_table_rows: 25
            max_table_cols: 6

    # ---------- parallel batch (opt-in) ----------
    parallel_batch:
        enabled: false
        self_url: ~               # required when enabled, e.g. 'http://127.0.0.1/rpc'
        min_batch_size: 2
        max_concurrency: 3
        budget: 10
        budget_store: apcu        # apcu | null | <service id>
        max_depth: 1
        connect_timeout: 0.5
        timeout: 10.0
        forward_headers: [Authorization, X-Request-Id, X-Forwarded-For, X-Forwarded-Proto, traceparent, tracestate]

    # ---------- observability (all opt-in) ----------
    logging:
        enabled: false
        channel: ~
        level_started: debug
        level_completed: info
        level_failed: warning
        log_params: true
        log_result: false
        slow_threshold_ms: ~

    sentry:
        enabled: false
        breadcrumbs: true
        tag_method: true
        transactions: false
        ignore_exceptions:
            - Knetesin\JsonRpcServerBundle\Exception\InvalidParamsException
            - Knetesin\JsonRpcServerBundle\Exception\InvalidRequestException
            - Knetesin\JsonRpcServerBundle\Exception\MethodNotFoundException
            - Knetesin\JsonRpcServerBundle\Exception\ParseException
            - Knetesin\JsonRpcServerBundle\Exception\AccessDeniedException
            - Knetesin\JsonRpcServerBundle\Exception\RateLimitExceededException

    opentelemetry:
        enabled: false
        tracer_name: 'json-rpc'
        traces: true
        metrics: true
        propagate_traceparent: true
        record_params: false
        record_result: false
        record_max_chars: 2048
        stream:
            record_row_count: true
            span_per_row: false
        ignore_exceptions: [...]  # same default set as Sentry

Per-key reference

security.roles_match

Default any. Used for #[Rpc\Method] when rolesMatch: is omitted.

Per-method override: #[Rpc\Method(rolesMatch: RoleMatch::All)].

security.expose_role_names

Default true (dev-friendly). When true, AccessDeniedException messages name the missing role(s): “One of the following roles is required: ROLE_X, ROLE_Y”. Flip to false in prod if your role identifiers leak business structure (ROLE_BILLING_INTERNAL) — the client then gets a generic Access denied. false also keeps role identifiers out of discovery: /mcp/tools entries carry no roles, and the OpenRPC document carries no x-rpc-roles / x-rpc-roles-match.

security.default_roles / public_prefixes / public_methods / prefix_roles

Default [] / [] / [] / {} — historical “no roles: on attribute means public” behavior. Set default_roles to flip the bundle into secure-by-default mode: every method that doesn’t carry its own #[Rpc\Method(roles: [...])] inherits the listed roles, with public_prefixes and public_methods as the allowlist for endpoints that should stay anonymous.

prefix_roles lets you narrow defaults per method-name prefix without touching every handler: e.g. admin.* → ROLE_ADMIN, internal.* → ROLE_INTERNAL, while everything else still falls back to default_roles. Longest matching prefix wins, so admin.users. overrides admin. for admin.users.create.

Resolution precedence (first match wins, computed at container compile time):

  1. attribute carries explicit roles → use as-is
  2. method name is in public_methods → public
  3. method name starts with any public_prefixes entry → public
  4. method name matches a prefix_roles entry (longest wins) → those roles
  5. default_roles is non-empty → apply default
  6. otherwise → public
json_rpc_server:
  security:
    default_roles: ['ROLE_USER']
    public_prefixes: ['public.', 'health.']
    public_methods: ['ping']
    prefix_roles:
      'admin.':    ['ROLE_ADMIN']
      'internal.': ['ROLE_INTERNAL']

The resolved roles end up in MethodMetadata::$roles — debug:rpc and the profiler show the materialized value, not the source.

max_request_size

Default 1048576 (1 MiB). Body size limit in bytes for /rpc, /rpc/stream and /mcp/call. 0 disables.

Per-method override via #[Rpc\MaxRequestSize(bytes: 65536)]. When the global limit is non-zero, the bundle’s parser cap is raised at compile time to fit the largest per-method maximum (so a method declaring a higher limit isn’t rejected before reaching the dispatcher). When the global limit is 0 (i.e. uncapped), the parser cap stays 0 regardless of per-method values — otherwise a single method with a small cap would silently cap every other method at the parser stage.

Inside a batch, each item is checked against its method’s limit by the item’s own size (its re-encoded JSON envelope); an oversized item gets a per-item -32600 and the rest of the batch runs. An item that cannot be re-encoded — a number beyond float range such as 1e400 decodes to INF — gets a per-item -32602, since its size cannot be checked. HTTP 413 is returned only when nothing ran: the body exceeds the parser cap, or a single (non-batch) request exceeds its method’s limit.

max_json_depth

Default 32. Maximum json_decode nesting depth for incoming RPC / MCP payloads. Raise only if your clients legitimately send deep structures.

http_status.enabled

Default false. When true, /rpc maps JSON-RPC error.code values to HTTP status codes (400/403/404/429/500) similar to /rpc/stream. Oversized non-batch bodies still return 413 even when this flag is off. Batch responses use the highest status among items. Leave disabled in production JSON-RPC clients unless you know your callers expect non-200 transports.

json_rpc_server:
  http_status:
    enabled: true

json.encode_flags

Default 96 (JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES). Bitmask of json_encode flags for response bodies. JSON_THROW_ON_ERROR is always OR-ed in by the bundle so encoding failures never silently produce false.

Common additions:

Flag Value When
JSON_PRETTY_PRINT 128 pretty-print responses (dev)
JSON_PRESERVE_ZERO_FRACTION 1024 force 1.0 over 1 in floats
JSON_HEX_TAG 1 escape < > for safe HTML embedding

headers.deprecation

Default X-Rpc-Deprecated. Custom HTTP header that carries human-readable per-method deprecation reasons in the form method.name: reason; other.method: …. The standard Deprecation: true (RFC 9745) is always sent alongside. Override when your platform policy bans X-* prefixes.

context.request_id_header

Default X-Request-Id. HTTP header read to populate Context::$requestId. Resolution order (first non-empty wins):

  1. Cached value from a prior call in the same request — every item in a JSON-RPC batch shares one requestId.
  2. The configured header — lets an API gateway pin its correlation id end-to-end.
  3. Freshly generated bin2hex(random_bytes(8)).

Set to an empty string to disable header lookup entirely.

params.allow_positional_dto

Default false. Whether handlers with a single DTO parameter accept positional JSON-RPC params ("params": [...]). Forbidden by default because positional params lock the DTO constructor order into the public API.

Per-method override: #[Rpc\Method(allowPositionalDto: true)].

params.reject_unknown

Default true. Whether DTO denormalization rejects unknown fields. The default catches client typos and stale legacy keys. Override per-method via #[Rpc\Method(rejectUnknown: false)] for backward-compatible endpoints.

serializer.datetime_format

Default iso8601. Output format for DateTimeInterface.

Value Output Input accepted
iso8601 2026-05-21T15:00:00+03:00 ISO, RFC, "yesterday", etc.
timestamp unix seconds (int) int → seconds, string → DateTimeImmutable
timestamp_ms unix milliseconds (int) int → milliseconds, string → DateTimeImmutable
any date() format per the format matching string strict-first, then lenient

serializer.date_format

Default Y-m-d. Output format for Type\Date (date without time). On input strings are tried strictly first, then through DateTimeImmutable as fallback (so 2026-05-21, 21.05.2026, 2026/05/21 all parse). Numbers are accepted as timestamps and truncated to date in the configured timezone.

serializer.timezone

Default null (source-value timezone untouched). When set (e.g. UTC) the timezone is applied when normalizing DateTimeInterface to a string and when truncating timestamps to dates. Setting to UTC is strongly recommended in cross-TZ deployments.

handlers.public

Default false. Whether RPC handler services are public in the container. Handlers are reached only via the bundle’s ServiceLocator — false keeps them off the public service API. Flip to true only if you need to fetch a handler directly from Container::get().

handlers.shared

Default false. Whether handler services are shared (singleton). Default false is required for long-running PHP runtimes (RoadRunner, FrankenPHP, Swoole) — a shared handler with mutable state would leak data between requests. Flip to true only if your handlers are guaranteed stateless and you want the per-process instantiation cost saved.

routes.{name}.path / routes.{name}.enabled

These keys only affect routes that Symfony already loads from config/routes/json_rpc_server.yaml (created by the Flex recipe or added manually — see Getting started).

Each route node accepts either a string (= path, enabled) or an object { path: ..., enabled: false }. enabled: false skips the bundled route entirely — useful if you define your own that points at the bundle controller.

cache.default_pool

Default cache.app. PSR-6 pool service id used when a method’s #[Rpc\Cache] doesn’t specify pool:.

cache.pools

Default {}. Named map of additional pools that #[Rpc\Cache(pool: 'name')] can reference. Values are service ids.

json_rpc_server:
    cache:
        pools:
            hot: cache.redis_hot
            warm: cache.redis_warm

cache.max_readable_key_length

Default 200. Maximum length of a human-readable cache key. Longer keys (or those carrying characters PSR-6 reserves) are hashed into <hash_prefix>.<sha1>. Lower for backends with strict key budgets, raise when keys would benefit from being greppable.

cache.key_prefix

Default rpc.cache. Prefix prepended to every cache key the bundle stores. Change when multiple instances of the bundle share a pool and you need separate namespaces (e.g. json_rpc_server.cache.tenant_a / json_rpc_server.cache.tenant_b).

cache.hash_prefix

Default rpc. Prefix used when a cache key gets hashed. Final key shape: <hash_prefix>.<sha1>.

rate_limiter.cache_pool

Default cache.app. PSR-6 pool used as the Symfony rate-limiter storage. Point at a dedicated Redis-backed pool for production workloads that need counters shared across processes.

stream.headers

Map of additional response headers to set on every streamed response. Set a value to ~ (null) to drop a default. Default disables nginx output buffering and HTTP caches so each chunk reaches the client immediately:

json_rpc_server:
    stream:
        headers:
            X-Accel-Buffering: no
            Cache-Control: no-cache
            # Add your own:
            Access-Control-Allow-Origin: '*'
            # Drop a default:
            X-Accel-Buffering: ~

profiler.enabled

Default true. Enables the Symfony Web Profiler integration. No-op outside kernel.debug, so safe to leave on in prod.

mcp.enabled

Default true. Set false to remove MCP services and routes entirely.

mcp.format_header / mcp.format_query

Defaults X-Mcp-Format and format. HTTP header / query-string name read to pick the MCP result format per request (highest priority in the format resolution chain). Override when your platform/proxy strips or rewrites X-* headers.

mcp.default_format

Default json. Result format used when neither the header / query / per- method attribute sets one. One of: json, pretty_json, markdown, plain, toon.

mcp.apply_rate_limit

Default true. Whether to apply #[Rpc\RateLimit] when a method is called via /mcp/call, so the MCP endpoint is limited like /rpc. Set false only when /mcp/call is reachable exclusively by a trusted internal agent.

mcp.expose_all / exclude_prefixes / exclude_methods / whitelist_methods

The MCP filter chain — see chapter 08 for the priority order.

mcp.schema_max_depth

Default 6. Maximum nesting depth JsonSchemaBuilder walks into a DTO. Guards against self-referencing DTOs that would otherwise recurse forever.

mcp.markdown.max_table_rows / max_table_cols

Defaults 25 and 6. Above these the markdown MCP format falls back to JSON instead of rendering an unwieldy table.

parallel_batch.enabled

Default false — parallel batch is off unless you turn it on. See Methods → parallel batches before enabling. Requires symfony/http-client, self_url and a non-empty kernel.secret (framework.secret), which keys the HMAC that authenticates the client IP forwarded to sub-calls.

parallel_batch.self_url

Default null; required when enabled: true. The absolute http:// / https:// URL sub-calls are POSTed to, including the RPC path — e.g. http://127.0.0.1/rpc, or a dedicated worker pool such as http://127.0.0.1/internal/rpc-fanout. The container build fails when it is missing or not an absolute http(s) URL. An env-var value can only be checked at runtime: if it is not an absolute http(s) URL, an error is logged and batches run sequentially — single calls are unaffected. It is never derived from the incoming request: the Host header is client-controlled and sub-calls carry the forwarded Authorization header.

json_rpc_server:
  parallel_batch:
    enabled: true
    self_url: '%env(RPC_SELF_URL)%'

parallel_batch.budget / budget_store

Defaults 10 and apcu. budget caps in-flight sub-calls across all concurrent batches; 0 disables the cap. budget_store: apcu checks APCu at runtime in the serving process: when APCu is unusable there, batches run sequentially and a warning is logged once per process. null means no system-wide cap; any other value is the id of a service implementing BudgetTrackerInterface.

parallel_batch.forward_headers

Incoming headers re-sent on every sub-call. Default: Authorization, X-Request-Id, X-Forwarded-For, X-Forwarded-Proto, traceparent, tracestate. Cookie is not in the default: with PHP’s native (locking) session handler the parent holds the session lock and every sub-call blocks on it until timeout. Cookie-session apps add Cookie explicitly, and only with non-locking or read-only sessions. The internal headers X-Rpc-Fanout-Depth, X-Rpc-Fanout-Client-Ip and X-Rpc-Fanout-Signature are always set by the bundle and never copied from the client.

Observability

See chapter 11 for the logging.*, sentry.*, and opentelemetry.* keys with full context.