Troubleshooting
Common issues and fixes when developing with ActingWeb.
401 at /mcp
Cause: No authentication configured or provided. MCP is OAuth2-protected in production.
Fix: Configure OAuth2 (Google/GitHub) with .with_oauth(…). Unauthenticated requests should return 401 with WWW-Authenticate header. For local, temporarily allow open access or sign in via /www.
DynamoDB Local connection errors
Symptom: Timeouts or table not found.
Fix: Ensure DynamoDB Local is running and set env vars:
export AWS_ACCESS_KEY_ID=local export AWS_SECRET_ACCESS_KEY=local export AWS_DEFAULT_REGION=us-east-1 export AWS_DB_HOST=http://localhost:8000
Slow first request after startup
Explanation: The permission system compiles/caches trust types on first use.
Fix: This is initialized automatically during framework integration. If you still see warmups during requests, check logs for initialization errors.
Tools/prompts don’t appear in tools/list or prompts/list
Check that you decorated hooks correctly: - Tools: @app.action_hook(“name”) + @mcp_tool(…) - Prompts: @app.method_hook(“name”) + @mcp_prompt(…)
Verify unified access control isn’t filtering them out for the current peer.
MCP client sees an empty tool list or -32003 after upgrading
Symptom: An MCP client that worked before an upgrade now sees an empty
tools/list/resources/list/prompts/list, or gets-32003(“Access denied”) ontools/call/prompts/get/resources/read.Cause: As of
v3.13.0rc3, a valid MCP access token that cannot be resolved to a trust relationship is fail-closed (no access) instead of fail-open (full access) — see theSECURITYsection ofCHANGELOG.rst. The server log has aWARNINGnaming the actor id and client id (“no trust found for MCP client … requests will be denied”) — but it is logged only when the trust resolver actually runs (a cache miss), not on every denied request. A negative result is cached for a short window (a few seconds), so if a client keeps retrying you’ll see the WARNING recur roughly that often rather than on every single request — check the logs over a short window, not just the exact moment you reproduce the symptom.Fix: Work through the pre-upgrade checklist in
docs/migration/v3.13.rst(“Security: MCP trust-cache authorization bypass”) — it covers finding at-risk trust rows, auditing Flask resource URIs against your trust type’s allowed patterns, and what happens to per-peer permission overrides when a client re-authorizes. In most cases the client simply needs to complete OAuth2 authorization again (not just retry the request) so its trust row is recreated with theoauth_client_idthe new resolver requires.
MCP tool’s structured data disappeared after upgrading
Symptom: After upgrading to
v3.13.0rc4or newer, a tool that used to deliver structured fields to the model now delivers only its text, andstructuredContentis absent from thetools/callresponse.Cause:
structuredContentis now opt-in. Extra top-level keys returned alongsidecontentused to be promoted intostructuredContentautomatically; that promotion was removed because clients that discard text blocks whenstructuredContentis present were silently dropping the tool’s entire prose payload. Dropped extras are not logged — they are a deliberate migration, not an error.Fix: Nest the data under an explicit
structuredContentkey, and keep the same object serialized in a textcontentblock so clients that ignorestructuredContentstill receive it. See “MCP:structuredContentis now opt-in” indocs/migration/v3.13.rst. If the tool’s real payload is prose, returning prose alone is now the correct shape.Note when verifying with ``curl``: a request with no
MCP-Protocol-Versionheader negotiates2025-03-26, which suppressesstructuredContenteven when set explicitly. Send-H 'MCP-Protocol-Version: 2025-06-18'or you will misread a working hook as broken.
MCP client rejects a tool with “has an output schema but did not return structured content”
Symptom: A spec-conforming client (Claude Code, the reference Python client) errors on every call to one tool with
Tool X has an output schema but did not return structured content. The server log carries a matchingWARNINGnaming the tool.Cause: The tool declares
output_schemaon@mcp_tool— sooutputSchemais advertised intools/list— but its hook returns nostructuredContent. Declaring a schema does not cause structured output to be emitted; the two are independent. Once a schema is advertised, clients treat structured output as mandatory on every non-error result.Fix: Either return an explicit
structuredContentconforming to the declared schema, or removeoutput_schemafrom the decorator. The warning is logged once per tool per process, so check the log from around process start rather than only the moment you reproduce it.
Property changes not visible in Web UI
Hook returns None for GET hides the property completely.
Hook returns None for PUT/POST marks the property read-only.
Subscription Callbacks Not Delivered
Symptom: Property changes on the publisher are not reaching subscribers.
Causes and fixes:
Circuit breaker is open: Check the circuit breaker status for the peer:
from actingweb.fanout import FanOutManager manager = FanOutManager(actor) status = manager.get_circuit_breaker_status(peer_id) if status == "OPEN": # Peer is unavailable, reset if issue resolved manager.reset_circuit_breaker(peer_id)
Peer URL unreachable: Verify the peer’s callback URL is accessible and responding.
Trust not approved: Ensure the trust relationship is approved on both sides.
Subscription target mismatch: Verify the subscription target matches what you’re publishing.
Unexpected Resync Callbacks
Symptom: Full resync callbacks triggered frequently instead of incremental diffs.
Causes and fixes:
Network latency: If callbacks arrive out-of-order often, increase the gap timeout:
app.with_subscription_processing(gap_timeout_seconds=10.0) # Default is 5.0
Burst updates: Rapid property changes can cause sequence gaps. Consider using suspension:
actor.subscriptions.suspend(target="properties") # ... perform bulk updates ... actor.subscriptions.resume(target="properties") # Sends single resync
Publisher restart: After a publisher restarts, sequence numbers reset. Subscribers should handle resync gracefully.
Duplicate Callbacks Being Processed
Symptom: Same data processed multiple times in your
@subscription_data_hook.Causes and fixes:
auto_sequence disabled: Ensure subscription processing is enabled:
app.with_subscription_processing(auto_sequence=True) # Default
Using raw callback_hook: The raw
@callback_hook("subscription")doesn’t deduplicate. Migrate to@subscription_data_hook.Multiple hook registrations: Check you haven’t registered the same hook twice.
Peer Data Not Cleaned Up After Trust Deletion
Symptom: RemotePeerStore data persists after trust relationship is deleted.
Causes and fixes:
auto_cleanup disabled: Enable automatic cleanup:
app.with_subscription_processing(auto_cleanup=True) # Default
Custom lifecycle hook overriding: If you have a
@app.lifecycle_hook("trust_deleted"), ensure it doesn’t prevent default cleanup.Manual cleanup needed: For legacy data, manually clean up:
from actingweb.remote_storage import RemotePeerStore store = RemotePeerStore(actor, peer_id) store.delete_all()
Subscriber Returning 429 (Too Many Requests)
Symptom: Publisher receives 429 responses when sending callbacks.
Causes and fixes:
Pending queue full: The subscriber’s
max_pendinglimit was exceeded:# On subscriber, increase if needed: app.with_subscription_processing(max_pending=200) # Default is 100
Processing too slow: The subscriber’s hook is blocking. Keep hooks fast or offload work.
Publisher retry strategy: Implement exponential backoff when receiving 429:
# FanOutManager handles retries automatically # For custom implementations, wait before retrying
Sequence Gaps Not Resolving
Symptom: Callbacks stuck in pending queue, never processed.
Causes and fixes:
Gap timeout too long: Reduce the timeout to trigger resync sooner:
app.with_subscription_processing(gap_timeout_seconds=3.0)
Missing callbacks: The publisher may have lost callbacks. Manually trigger resync:
# On publisher side: actor.subscriptions.resume(target="properties")
Check pending state: Use devtest endpoints to inspect pending queue (devtest mode only):
GET /{actor_id}/devtest/callback_state/{peer_id}/{subscription_id}