{"id":51183413,"url":"https://github.com/justinmchase/real-polite-protocol","last_synced_at":"2026-06-27T08:31:12.658Z","repository":{"id":362515197,"uuid":"1201463656","full_name":"justinmchase/real-polite-protocol","owner":"justinmchase","description":"The second most Minnesota protocol of all time","archived":false,"fork":false,"pushed_at":"2026-06-04T14:03:20.000Z","size":1249,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-04T16:03:23.312Z","etag":null,"topics":["mcp","polite","protocol","rpp"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/justinmchase.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-04T18:00:05.000Z","updated_at":"2026-06-04T14:04:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/justinmchase/real-polite-protocol","commit_stats":null,"previous_names":["justinmchase/real-polite-protocol"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/justinmchase/real-polite-protocol","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justinmchase%2Freal-polite-protocol","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justinmchase%2Freal-polite-protocol/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justinmchase%2Freal-polite-protocol/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justinmchase%2Freal-polite-protocol/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/justinmchase","download_url":"https://codeload.github.com/justinmchase/real-polite-protocol/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justinmchase%2Freal-polite-protocol/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34847287,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-27T02:00:06.362Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["mcp","polite","protocol","rpp"],"created_at":"2026-06-27T08:31:11.902Z","updated_at":"2026-06-27T08:31:12.644Z","avatar_url":"https://github.com/justinmchase.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RPP — Real Polite Protocol\n\nRPP is a lightweight, open protocol for structured, courteous machine-to-machine\ncommunication.\n\nThis repository contains:\n\n- **The spec** — a formal definition of the RPP protocol\n- **A reference implementation** — a lightweight implementation of the spec,\n  targeting [Deno Deploy](https://deno.com/deploy)\n\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.\n\n---\n\n## Spec-Driven Development\n\nRPP is built spec-first. The protocol is defined as a written specification,\nthat spec is decomposed into machine-readable requirement documents, and every\nrequirement is verified by an executable test. Implementation code is the\n*last* artifact in the chain, not the first.\n\nThis is enforced by a strict authority order that applies to every change in\nthe repository:\n\n1. **RFC / spec** — [spec/rpp-spec.md](spec/rpp-spec.md)\n2. **Requirement documents** — [.github/requirements/](.github/requirements/)\n3. **Requirement tests** — [src/requirements/](src/requirements/)\n4. **Scenarios** — [.github/scenarios/](.github/scenarios/) +\n   [src/scenarios/](src/scenarios/)\n5. **Implementation code** — [src/](src/)\n\nA lower-authority artifact MUST NOT contradict a higher-authority one. When a\ntest fails, the implementation gets fixed — the test is not weakened. When the\ndesired behavior conflicts with a requirement, the requirement (and possibly\nthe spec) is changed deliberately and explicitly, then the tests and code\nfollow. This keeps the spec, the requirements, the tests, and the running code\nin lock-step.\n\n### Requirement documents\n\nEach requirement is a small Markdown file under `.github/requirements/`,\ngrouped into folders by feature area (`invitations/`, `messages/`, `contacts/`,\n`receptive-policy/`, `domain-admin/`, ...). Every document carries\nfrontmatter with a stable `id`, a human-readable `title`, and a `spec_ref`\npointing back to the relevant section(s) of the RPP spec:\n\n```yaml\n---\nid: invitations-003\ntitle: Listeners can accept a pending invitation\nspec_ref: \"10.2, 10.4, 11.2, 12.2\"\n---\n```\n\nThe body describes the expected behavior in plain language — what the system\nmust do, what inputs it accepts, what error codes it returns, and what state\ntransitions it performs — at a level of detail that both humans and agents can\nimplement and verify against.\n\n### Requirement tests\n\nEvery requirement document has a mirrored test file under `src/requirements/`\nthat uses the exact same path. For example:\n\n| Requirement document                                                  | Test file                                                                |\n| --------------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| `.github/requirements/startup.requirement.md`                         | `src/requirements/startup.requirement.test.ts`                           |\n| `.github/requirements/invitations/003-accept-invitation.requirement.md` | `src/requirements/invitations/003-accept-invitation.requirement.test.ts` |\n\nEach test file has a single top-level `Deno.test()` named after the\nrequirement id, with individual assertions grouped into `t.step()` calls:\n\n```ts\nDeno.test(\"req:invitations-003 - Listeners can accept a pending invitation\", async (t) =\u003e {\n  await t.step(\"creates a bilateral contact\", async () =\u003e { ... });\n  await t.step(\"dispatches an invitation_reply envelope\", async () =\u003e { ... });\n  await t.step(\"rejects non-pending invitations with E_INVITATION_NOT_PENDING\", async () =\u003e { ... });\n});\n```\n\nRequirement tests are integration-scoped: they exercise the real controllers,\nmanagers, and repositories against an isolated Deno KV store. Reusable test\nhelpers live in [src/requirements/helpers/](src/requirements/helpers/) — test\nfiles themselves contain only imports and `Deno.test()` calls, never inline\nhelpers or shared fixtures.\n\n### Scenarios\n\nWhere requirement tests verify a single normative behavior in-process,\n**scenarios** verify whole-system, multi-user journeys end-to-end through the\nrunning MCP HTTP endpoint. Each scenario is a Markdown document under\n`.github/scenarios/` describing a real-world flow in plain English (e.g.\n\"Alice opens a receptive window; Justin sends her an invitation; Alice\naccepts and replies\"), paired with a mirrored\n`src/scenarios/**/\u003cname\u003e.scenario.test.ts` that drives the journey using\nauthenticated persona clients. Scenarios sit below requirement tests in the\nauthority chain and complement — but never replace — them.\n\n### Coverage and gap analysis\n\nThe repository is audited continuously against its own spec. Two reports live\nunder [spec/reports/](spec/reports/) and are regenerated as the codebase\nevolves:\n\n- [spec/reports/gap-analysis-report.md](spec/reports/gap-analysis-report.md) —\n  structural coverage: which RFC sections have requirement docs, which\n  requirement docs have tests, and what is missing.\n- [spec/reports/evaluate-report.md](spec/reports/evaluate-report.md) —\n  semantic coverage: how thoroughly each requirement test actually verifies\n  the *meaning* of its requirement document, with concrete suggestions for\n  closing gaps.\n\nCoverage is reported in the form `X/Y requirements covered by tests (Z%)`,\nand significant gaps (missing requirements, weak normative language, untested\nbehaviors) are called out explicitly so they can be addressed before they\nbecome regressions.\n\n### Working with the system\n\nWhen adding a new behavior, the workflow is always:\n\n1. If the spec does not already mandate the behavior, propose a spec change\n   first.\n2. Add or update a requirement document under `.github/requirements/` with a\n   stable `id` and a `spec_ref` back to the relevant spec section(s).\n3. Add or update a mirrored requirement test under `src/requirements/` that\n   fails until the behavior is implemented.\n4. Implement the behavior in `src/` until the test passes.\n5. Where the behavior spans multiple personas or HTTP round-trips, add a\n   scenario under `.github/scenarios/` + `src/scenarios/` as well.\n6. Re-run the gap-analysis and evaluate reports and address any regressions.\n\nRun everything together with:\n\n```sh\ndeno test\n```\n\nNo special configuration is needed — Deno discovers all `*.test.ts` files\n(requirement tests, scenario tests, and unit tests) automatically.\n\n---\n\n## Local Data\n\nWhen you run the server locally with `deno task start`, the default Deno KV\ndatabase is stored at `.data/kv.sqlite3`.\n\nSet `RPP_KV_PATH` to override that path when needed.\n\n---\n\n## Local Test Users\n\nFor manual end-to-end testing you often need a second identity besides the Azure\nAD user backing your VS Code MCP session. The repo ships a dev-only auth seam\nplus a small CLI for acting on the running server as a named persona.\n\nEnable the seam (refuses to activate on Deno Deploy):\n\n```sh\ndeno task dev          # same as `deno task start` with RPP_DEV_MODE=1\n```\n\nThen in another terminal, act as a persona:\n\n```sh\ndeno task as alice token                     # mint and print a JWT\ndeno task as alice get-permissions\ndeno task as alice set-display-name \"Alice\"\ndeno task as alice open-window --duration 3600\ndeno task as alice send-invitation \\\n  --to localhost:8000 --shortcode \u003ccode\u003e --terms \"hello\"\ndeno task as alice list-messages --unread\ndeno task as alice reply \u003cmessage-id\u003e --body \"got it\"\ndeno task as alice call \u003cany-tool\u003e --arg key=value\n```\n\nPersona files live in `.dev/users/\u003cname\u003e.json` (gitignored, auto-created on\nfirst use). Hand-edit `roles` to add `\"domain.admin\"` when exercising admin-only\ntools. The signing keypair lives in `.dev/keys/` and is generated on first use.\nBoth directories are gitignored.\n\nOverride defaults with env vars: `RPP_SERVER` (default `http://localhost:8000`),\n`RPP_DEV_ISSUER` (default `urn:rpp:dev`), `RPP_DEV_PUBLIC_KEY_PATH`.\n\n---\n\n## MCP Tools Reference\n\nRPP exposes its functionality as [MCP](https://modelcontextprotocol.io) tools.\nThe following tools are available to authenticated users via the `/mcp`\nendpoint.\n\n### Account\n\n| Tool                         | Description                                                                                                                                                                          |\n| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `get_permissions`            | Return current account permission levels.                                                                                                                                            |\n| `get_display_name`           | Get the display name for the authenticated account.                                                                                                                                  |\n| `set_display_name`           | Set or clear the display name for the authenticated account. Pass null to clear.                                                                                                     |\n| `set_user_verified_metadata` | Refresh your user-sourced verified metadata record from your current token claims (name, email, preferred_username, ctry). No arguments required — the token is the source of truth. |\n\n### Contacts\n\n| Tool                | Description                                                                                                                                                                    |\n| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `list_contacts`     | List contacts owned by the authenticated account. Returns the bilateral contact records established via accepted invitations (spec §11). Supports filtering by blocked status. |\n| `get_contact`       | Retrieve a single contact by id. Returns 404 when not owned by the caller.                                                                                                     |\n| `set_contact_field` | Record an owner-supplied field value for a contact. The new record is appended as source `owner_note`; existing values from other sources are preserved.                       |\n| `remove_contact_field_revision` | Permanently remove a single historical revision from a contact's field history, identified by `(contact_id, key, recorded_at)`. Works on any revision, not just the most recent (spec §11.7).            |\n| `block_contact`     | Mark a contact as blocked. Inbound and outbound messaging through this contact is refused until the contact is unblocked (spec §11.6).                                         |\n| `unblock_contact`   | Clear the blocked flag on a contact, restoring inbound and outbound messaging.                                                                                                 |\n| `delete_contact`    | Permanently delete a contact and its history. Both directions of communication become unsignable; pending invitations are unaffected.                                          |\n\n### Invitations\n\n| Tool                    | Description                                                                                                                                                                                                                        |\n| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `list_invitations`      | List inbound invitations addressed to the authenticated account. Filter by status or remote domain.                                                                                                                                |\n| `list_sent_invitations` | List outbound invitations sent by the authenticated account.                                                                                                                                                                       |\n| `review_invitation`     | Get detailed information about a specific invitation before accepting or rejecting.                                                                                                                                                |\n| `accept_invitation`     | Accept an inbound invitation. Creates a bilateral contact, generates a fresh local credential, and dispatches an `invitation_reply` envelope to the remote (spec §10.4 / §11.2 path 1).                                            |\n| `reject_invitation`     | Reject an inbound invitation. The rejection is recorded locally; no envelope is sent to the remote.                                                                                                                                |\n| `send_invitation`       | Send an invitation envelope to a remote RPP domain (spec §10.1). Provide exactly one of `receptive_policy_id` or `shortcode`. The local domain generates a fresh reply_credential the remote will use to authenticate their reply. |\n| `cancel_invitation`     | Cancel a pending outbound invitation (spec §10.3). Re-sends the invitation envelope with `cancelled: true` to the remote, then transitions the local outbound record to cancelled.                                                 |\n| `invite_contact`        | Re-invite a known contact using a fresh `receptive_policy_id` shared out-of-band (spec §11.3 / contacts-006). Convenience wrapper around `send_invitation` that resolves `receiver_domain` from the stored contact.                |\n\n### Messages\n\n| Tool                 | Description                                                                                                                                                                                                            |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `list_messages`      | Pure-query: list messages received by the authenticated account, ordered by received_at descending. Does NOT mark messages as read. Use for unread counts, filtering, or pagination without consuming.                 |\n| `read_messages`      | Fetch messages for presentation to the user AND mark them all as read. Accepts the same filters as `list_messages`. Call this when displaying message content.                                                         |\n| `get_message`        | Retrieve a single received message by its wire message_id. Does NOT mark the message as read.                                                                                                                          |\n| `read_message`       | Retrieve a single received message by its wire message_id AND mark it as read. Use when presenting the message body to the user.                                                                                       |\n| `mark_read`          | Mark one or more received messages as read.                                                                                                                                                                            |\n| `delete_message`     | Permanently delete a received message from the local inbox. Deletion is local-only.                                                                                                                                    |\n| `send_message`       | Send a `message` envelope to a contact. The local domain HMAC-signs the request with the contact's remote_credential (spec §11.3 / §11.5).                                                                             |\n| `list_sent_messages` | List messages dispatched by the authenticated account, ordered by sent_at descending.                                                                                                                                  |\n\n### Receptive Policies\n\n| Tool                      | Description                                                                                                                                                                                                                                                           |\n| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `get_receptive_policies`  | List all receptive policies for the authenticated user. Returns an empty list if no policies have been added (implying closed/not receptive).                                                                                                                         |\n| `add_receptive_policy`    | Add a new receptive policy. Policies stack — multiple can be active simultaneously. Supports modes: `all`, `domain_filter`, `contact`, or `closed`.                                                                                                                   |\n| `open_receptive_window`   | Open a time-bounded receptive window that stacks with existing policies. Returns a shortcode (8 lowercase alphanumeric characters) tied to this window. The user must share both the shortcode and the server domain with the person who wants to send an invitation. |\n| `remove_receptive_policy` | Remove a receptive policy. For time-bounded windows this closes the window early.                                                                                                                                                                                     |\n\n### Domain Admin\n\n\u003e These tools are only available to users with the domain admin role.\n\n| Tool                             | Description                                                                                           |\n| -------------------------------- | ----------------------------------------------------------------------------------------------------- |\n| `get_domain_identity`            | Retrieve the current domain identity as published by the server.                                      |\n| `update_domain_identity`         | Update mutable domain identity fields.                                                                |\n| `get_contact_policy_url`         | Retrieve the current `contact_policy_url` from domain identity.                                       |\n| `set_contact_policy_url`         | Set or update the `contact_policy_url`.                                                               |\n| `get_verification_key`           | Retrieve the active public verification key and key identifier.                                       |\n| `rotate_verification_key`        | Generate a new Ed25519 keypair for domain-verified invitations. Archives the previous active key.     |\n| `list_historical_keys`           | List archived verification keys with key_id and archived_at metadata.                                 |\n| `delete_historical_key`          | Remove an archived verification key by key_id. Prior attestations using that key become unverifiable. |\n| `list_verifiable_users`          | List users whose metadata the server can verify, along with their verifiable fields.                  |\n| `get_user_verified_metadata`     | Retrieve verified metadata for a specific user by oid.                                                |\n| `set_admin_verified_metadata`    | Create or update admin-supplied verified metadata fields for a specific registered user.              |\n| `remove_admin_verified_metadata` | Remove one admin-verified metadata field for a specific registered user.                              |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjustinmchase%2Freal-polite-protocol","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjustinmchase%2Freal-polite-protocol","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjustinmchase%2Freal-polite-protocol/lists"}