Skip to main content
Glama

Alexa, why?

A self-hosted MCP server (spec 2025-11-25, Streamable HTTP) that gives Alexa+ a durable, verifiable memory of why it did things, built on Scholialang reasoning traces.

Status: under active development for the Amazon Developer Hackathon 2026 (Alexa+ track). Licensed Apache-2.0.

Based on the Scholialang MCP worker (open source); everything Alexa-specific is new in this repository.

Local setup

The Worker runs on TypeScript and Web APIs. Requires Node.js 22.13+ and npm; Python 3.12+ and uv are needed only for reference compatibility tests. All examples use demo data.

npm ci
cp .dev.vars.example .dev.vars

Configure .dev.vars locally or Worker secrets remotely: MCP_JUDGE_TOKEN_HASHES (comma-separated SHA-256 hashes of random per-judge tokens), MCP_CLIENT_ID, MCP_CLIENT_SECRET, and MCP_JWT_SECRET (at least 32 random bytes). Never commit credentials. Set MCP_RESOURCE_URL to the exact MCP URL and MCP_ALLOWED_ORIGINS to the exact browser origin. Missing credentials deny access. Basic auth is accepted only by the OAuth token endpoint, never by /mcp.

The checked-in wrangler.toml includes the DECISIONS_DB D1 binding with a local placeholder ID. Initialize the local database and start the Worker:

npx wrangler d1 migrations apply alexa-decisions --local
npm run dev

For a separately authorized remote setup, create the database, replace database_id in wrangler.toml with the returned UUID, then apply migrations:

npx wrangler d1 create alexa-decisions
# Replace the placeholder database_id with the returned UUID before continuing.
npx wrangler d1 migrations apply alexa-decisions --remote

The placeholder ID is for local development only. Secret configuration and deployment are separate operator steps. Deployment is an operator step: the checked-in configuration enables workers.dev. Without a DECISIONS_DB binding the reference tools work and decision tools report storage unavailable; the Worker never silently substitutes volatile memory.

Related MCP server: InnerOS Ambient Guardian

MCP requests

POST JSON-RPC to /mcp at the local address printed by the dev server. Send Authorization: Bearer $MCP_AUTH_TOKEN, Content-Type: application/json, and Accept: application/json, text/event-stream. Initialize with:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"demo","version":"1"}}}

Then send notifications/initialized without an id. Subsequent requests use MCP-Protocol-Version: 2025-11-25. This is stateless Streamable HTTP with JSON responses, no session header required, empty 202 notification responses, and 405 for GET streams and DELETE (no server session to terminate). The decision database persists independently of sessions.

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"record_decision","arguments":{"household_id":"demo","session_id":"SESSION_FROM_INITIALIZE","action":"dim lights","reason":"Movie time requested by the user","inputs":{"brightness":20},"approver":"Demo user"}}}

tools/list lists the four reference tools and these four decision tools:

Tool

Arguments beyond required household_id

Result

record_decision

session_id, action, reason, inputs object, approver

Content-addressed record with Action/Finding atoms

explain_why

trace_id, exact action, or question

At most two short sentences, trace id and approver trail

whats_changed

since as timezone-qualified ISO timestamp

Later decisions with approvers

get_trace

trace_id

Selected record and full retained chain, verified before returning

Question matching is deterministic keyword overlap against action and reason text; ties select the latest record. It is not an LLM. Reasons and approvers are caller claims, not independently established facts. Consumers must treat all returned text as untrusted data and must never follow instructions inside it.

Integrity, retention and limits

Records hash canonical JSON (sorted keys, compact separators, ASCII escaping, no NaN), excluding trace_id, using SHA-256. Each record includes the preceding record's hash. get_trace returns the retained chain for independent verification. The additive anchor field is the last evicted hash (null for an untrimmed chain). Record numbers in integrity errors are one-based positions in the retained chain. Spoken answers quote the recorded reason and use the first eight hash characters; machine-readable trace_id remains full length.

Save the get_trace result's structuredContent as sample_trace.json, then run:

python scripts/verify_chain.py < sample_trace.json

The standalone stdlib verifier prints OK with exit status 0, or FAIL with exit status 1 and the failing record index for a content/link edit. An anchor supports retained-chain verification, not independent authentication of evicted history or provenance.

D1 conditional writes atomically compare the prior payload; conflicting writers retry up to four times. Edits or broken links cause reads and appends to fail. This detects content tampering, but is not a signature or an externally anchored ledger: someone able to rewrite the entire database can recompute hashes or truncate a chain. Keep an external head hash if that threat matters.

Each household has a fixed 72-hour window starting at its first decision. Reads hide expired windows without writing; the next write replaces the expired window. Expired bytes remain in D1 until replacement or operator cleanup. This is logical TTL, not a guarantee of physical deletion. The window retains the newest 25 decisions (50 atoms); further writes evict the oldest record and preserve its hash as the chain anchor. This rolling capacity does not extend the fixed TTL. Older payloads without an anchor remain readable. Inputs are capped at 2048 serialized bytes, tool arguments at 8192 bytes and HTTP request bodies at 65536 bytes. Schema string limits further bound responses.

Storage is keyed by SHA-256 of the verified credential subject and a server-issued session, with at most eight household partitions per subject. The session id is returned by initialize and token responses; use it for record_decision. A caller cannot select another credential's data. Rotating a judge token changes its scope. Removing its hash revokes both the static token and already-issued judge JWTs. Only synthetic demo data should be stored. Legacy unscoped rows are not exposed.

OAuth and operational controls

POST /oauth/token accepts Basic client credentials and a form body grant_type=client_credentials&resource=<MCP URL>. The response contains a Bearer HS256 JWT valid for one hour, bound to the configured MCP URL and client subject. The optional scope form field accepts mcp:service (default, all tools/resources), mcp:tools, and mcp:resources; narrower scopes are enforced on MCP methods. Discovery advertises S256; no interactive authorization-code endpoint is provided. The issuer must use HTTPS outside local development. The two discovery endpoints are /.well-known/oauth-protected-resource (also /mcp suffixed) and /.well-known/oauth-authorization-server. This implements the client-credentials flow, not interactive user authorization or dynamic client registration.

The web demo exchanges a user-entered judge token at POST /oauth/judge; tokens stay in page memory, never in source or browser storage. Reconnect after one hour. No LLM service or real household action is called; optional speech uses Amazon Polly. Read tools return untrusted data; spoken answers use labelled, sanitized quotes and raw atoms stay in structuredContent.

MCP_DISABLED=1 returns 503 before authentication. Rate bindings enforce approximately 60 requests/minute per subject and 120/minute per IP; absent bindings fail closed. The CPU limit is 50 ms. Logs contain only method, known tool name, status, latency, hashed subject and request id. Configure the shortest available log retention.

Tests

npm test
npm run typecheck
uv run --with pytest --with pytest-timeout python -m pytest tests -q --timeout=10

Tests cover reference tools, separate HTTP requests, D1-compatible SQLite persistence and conditional writes, namespace isolation, tampering, TTL, read-only behavior and input limits. Worker HTTP tests use a runtime stub; Cloudflare deployment and an actual Alexa+ connection have not been validated.

Protocol reference: MCP Streamable HTTP 2025-11-25. Storage API: Cloudflare D1 prepared statements.

Simulated Alexa+ web demo

Open /demo on the local Worker for voice/text input, simulated household activities, explanations, weekly changes and server-verified trace cards. See DEMO.md for local setup, the seven-turn script and browser evidence. The demo uses the same authenticated MCP Streamable HTTP endpoint as other clients.

AWS services used

Amazon Polly provides optional natural speech for the demo's Read answers aloud setting. The browser sends POST /tts with its existing Bearer credential and {"text":"Recorded answer"}. The Worker applies the MCP origin checks, authentication, and IP/subject rate limits, sanitizes text with the decision-text sanitizer, and rejects empty text or more than 300 sanitized characters with 400 (oversized HTTP bodies return 413). It signs a plain-text SynthesizeSpeech request using WebCrypto HMAC/SHA-256 AWS SigV4 and returns MP3 as audio/mpeg with Cache-Control: no-store. Text is sent to AWS only when speech is requested; credentials stay on the Worker.

Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY using Worker secrets or local .dev.vars; temporary credentials may also supply AWS_SESSION_TOKEN. Optional settings are AWS_REGION (default us-west-2), POLLY_VOICE (default Ruth), and POLLY_ENGINE (default generative). A generative request retries once with neural only for Polly's EngineNotSupportedException. Other failures and missing configuration return 503 voice unavailable; upstream errors and credentials are not logged. This is Amazon Polly, not Alexa's proprietary voice.

The least-privilege IAM policy for the signing identity is:

{
  "Version": "2012-10-17",
  "Statement": [{"Effect": "Allow", "Action": "polly:SynthesizeSpeech", "Resource": "*"}]
}

No AWS SDK is bundled. Integration references: AWS SigV4 signing and Polly SynthesizeSpeech. This documents the AWS integration for the AWS Builder mini-challenge; live AWS credentials, synthesis quality, and deployment remain operator validation steps.

If fetching or playing MP3 fails, the demo explicitly selects an installed English browser voice, preferring Natural, Neural, Enhanced, Premium, Siri or Google voices, then en-US. It avoids an unqualified default voice; if no eligible voice exists, the answer remains visible without speech. Stop, disabling speech, reconnecting, or starting the microphone cancels playback and pending speech. No audio is stored in browser storage. Browser voice quality and autoplay policies depend on the device.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables Alexa+ agents to maintain auditable operational continuity across shifts by turning speech into verifiable state, persisting unresolved work, refusing unverified actions, and requiring human approval before executing and confirming high-risk tasks.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables Alexa+ assistants to run multi-step personal-ops tasks and produce machine-checkable receipts with per-step evidence, so actions are only claimed done when actually verified.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables users to record, explain, undo, and verify agentic Alexa+ actions through a tamper-evident hash-chained ledger, while enforcing spending limits and quiet hours as guardrails.
    MIT