Skip to content

feat(mcp): manage people (rename, register, remove) via MCP (#4862)#8491

Open
ZachL111 wants to merge 6 commits into
BasedHardware:mainfrom
ZachL111:zach/mcp-people
Open

feat(mcp): manage people (rename, register, remove) via MCP (#4862)#8491
ZachL111 wants to merge 6 commits into
BasedHardware:mainfrom
ZachL111:zach/mcp-people

Conversation

@ZachL111

@ZachL111 ZachL111 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

The MCP server can read the people Omi identifies from conversations (recurring speakers) via get_people, but it cannot manage them. People are derived from speaker identification and frequently need curation: a speaker gets mislabeled, the same person shows up under two entries, or the user wants to register a known contact by name. This adds people management to the MCP server so an assistant can keep the user's people clean for the two-way memory bank (#4862).

What this adds

Five new MCP tools, on both transports (REST mcp.py and SSE mcp_sse.py) through one shared util:

  • get_person: fetch a single person by ID.
  • find_person_by_name: look up a person by exact name (returns the person, or null when there is no match).
  • create_person: register a known person by name. Idempotent by name (returns the existing person if one already has that name), matching the REST people endpoint, so an assistant cannot create duplicates.
  • update_person: rename a person, for example to relabel a speaker Omi identified.
  • delete_person: remove a person. Conversations attributed to them fall back to a generic speaker label.

New OAuth scope: people.write (people.read already existed for get_people).

Design

  • utils/mcp_people.py is the single orchestration point both transports call, so REST and SSE cannot drift. It mirrors the REST people router: a missing person is 404 / -32001, and string inputs are type-checked so a non-string argument from the SSE transport returns a clean error rather than an uncaught exception.
  • Output uses the existing clean_person shaper, which already drops raw speech-sample audio URLs and speaker embeddings.

Testing

Added tests to tests/unit/test_mcp_data_endpoints.py (the canonical MCP data test, already wired into test.sh), covering both transports: get/find/create/update/delete, idempotent-by-name create, the rename and delete not-found mappings, blank and non-string input rejection, find-by-name hit and miss, and tool/scope registration. The full file passes (33 tests). Black and the async-blocker lint are clean.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 4 files

Confidence score: 1/5

  • backend/routers/mcp_sse.py and backend/routers/mcp.py currently bypass people.write authorization on destructive people operations, so any valid MCP API key could modify data if merged as-is. This is a merge-blocking access-control gap—enforce scope checks in both SSE tool execution and REST handlers (and add regression tests) before merging.
  • In backend/routers/mcp_sse.py, create_person and update_person return read-level person data while requiring only people.write, which can leak data to callers without read permission. Tighten response fields or require people.read alongside write access before merging.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/routers/mcp.py">

<violation number="1" location="backend/routers/mcp.py:541">
P1: People write routes bypass the people.write scope enforcement on REST endpoints.</violation>
</file>

<file name="backend/routers/mcp_sse.py">

<violation number="1" location="backend/routers/mcp_sse.py:97">
P1: New `people.write` scope is advertised in tool metadata but not enforced during SSE tool execution. Any valid MCP API key can invoke destructive people operations.</violation>

<violation number="2" location="backend/routers/mcp_sse.py:926">
P2: `create_person` and `update_person` expose read-level person data while requiring only `people.write` scope</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread backend/routers/mcp.py


@router.post("/v1/mcp/people", tags=["mcp"])
def mcp_create_person(request: McpCreatePerson, uid: str = Depends(get_uid_from_mcp_api_key)):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: People write routes bypass the people.write scope enforcement on REST endpoints.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/routers/mcp.py, line 541:

<comment>People write routes bypass the people.write scope enforcement on REST endpoints.</comment>

<file context>
@@ -498,6 +499,69 @@ def get_people(uid: str = Depends(get_uid_from_mcp_api_key)):
+
+
+@router.post("/v1/mcp/people", tags=["mcp"])
+def mcp_create_person(request: McpCreatePerson, uid: str = Depends(get_uid_from_mcp_api_key)):
+    try:
+        return clean_person(mcp_people.create_person(uid, request.name))
</file context>

CHAT_READ_SECURITY = [{"type": "oauth2", "scopes": ["chat.read"]}]
SCREEN_ACTIVITY_READ_SECURITY = [{"type": "oauth2", "scopes": ["screen_activity.read"]}]
PEOPLE_READ_SECURITY = [{"type": "oauth2", "scopes": ["people.read"]}]
PEOPLE_WRITE_SECURITY = [{"type": "oauth2", "scopes": ["people.write"]}]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: New people.write scope is advertised in tool metadata but not enforced during SSE tool execution. Any valid MCP API key can invoke destructive people operations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/routers/mcp_sse.py, line 97:

<comment>New `people.write` scope is advertised in tool metadata but not enforced during SSE tool execution. Any valid MCP API key can invoke destructive people operations.</comment>

<file context>
@@ -92,6 +94,7 @@
 CHAT_READ_SECURITY = [{"type": "oauth2", "scopes": ["chat.read"]}]
 SCREEN_ACTIVITY_READ_SECURITY = [{"type": "oauth2", "scopes": ["screen_activity.read"]}]
 PEOPLE_READ_SECURITY = [{"type": "oauth2", "scopes": ["people.read"]}]
+PEOPLE_WRITE_SECURITY = [{"type": "oauth2", "scopes": ["people.write"]}]
 
 
</file context>

person = mcp_people.create_person(user_id, arguments.get("name"))
except mcp_people.PersonError as e:
raise _person_tool_error(e)
return {"success": True, "person": clean_person(person)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: create_person and update_person expose read-level person data while requiring only people.write scope

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/routers/mcp_sse.py, line 926:

<comment>`create_person` and `update_person` expose read-level person data while requiring only `people.write` scope</comment>

<file context>
@@ -836,6 +904,41 @@ def execute_tool(user_id: str, tool_name: str, arguments: dict) -> dict:
+            person = mcp_people.create_person(user_id, arguments.get("name"))
+        except mcp_people.PersonError as e:
+            raise _person_tool_error(e)
+        return {"success": True, "person": clean_person(person)}
+
+    elif tool_name == "update_person":
</file context>

@ZachL111

Copy link
Copy Markdown
Contributor Author

Thanks cubic. These three findings all come down to one design point about how the Omi MCP server does authorization, so let me address them together.

MCP API keys are user-scoped bearer credentials, not per-scope ACLs. authenticate_api_key validates the key and returns a single user_id; the McpApiKey model carries no scopes, and handle_mcp_message passes only that user_id into execute_tool. No tool, read or write, checks a scope at execution time, and that is already true for the existing write tools: create_memory, edit_memory, and delete_memory declare MEMORIES_WRITE_SECURITY the same metadata-only way. The securitySchemes entries are OAuth-discovery metadata advertised in scopes_supported, not per-key grants the server enforces.

Two consequences for these findings:

1 and 2 (people.write not enforced): people.write follows the identical pattern as the existing memories.write, so the people tools are consistent with the server's current model rather than introducing a new gap. Because the key is bound to one user, a key can only ever read or modify its own owner's data, so there is no cross-user exposure and no read-only-key concept to bypass. I raised this same point on the action-items PR (#8439) and it was accepted as a server-wide follow-up rather than a per-PR change. Adding enforcement for people-only would diverge from every other tool; real per-key scope enforcement is a server-wide change to the MCP auth layer that belongs in its own PR, applied across all tools at once with the maintainer's sign-off.

3 (write tools return person data): returning the created or updated resource is standard create/update behavior and matches create_memory returning the memory. With no per-key scopes there is no write-only caller to leak to, and clean_person already strips the sensitive fields (speech-sample audio URLs and speaker embeddings), so the response is the same lean shape get_people already returns.

Happy to implement server-wide MCP scope enforcement (threading granted scopes from the key through execute_tool and checking each tool's requirement) as a dedicated PR if you want that hardening landed across the whole tool surface.

@Git-on-my-level Git-on-my-level added needs-maintainer-review Requires human maintainer review before merge feature-fit-review PR touches feature direction; qualitative fit assessed labels Jun 28, 2026
@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks @ZachL111 — solid work here. The shared mcp_people.py orchestration layer is the right call (single source of truth so REST + SSE can't drift), the SSE raw-JSON type validation is thoughtful, clean_person correctly strips speech-sample audio URLs and speaker embeddings, and the tests cover both transports with good edge cases. Checks are green.

Two observations for the human maintainer before merge:

  1. Auth model is consistent — not a new gap. I verified that get_uid_from_mcp_api_key returns only a user_id and the MCP API key carries no per-scope grants. The existing write tools (create_memory, edit_memory, delete_memory) declare MEMORIES_WRITE_SECURITY the same metadata-only way, so PEOPLE_WRITE_SECURITY follows the established server pattern rather than introducing a divergence. A key is bound to one user, so there's no cross-user exposure. Server-wide per-scope enforcement is a reasonable follow-up but belongs in its own cross-cutting PR, as you noted on feat(mcp): create, complete, update, and search action items via MCP #8439 — not blocked on this PR.

  2. Delete speech-sample cleanup. The REST endpoint (delete_person_endpoint in routers/users.py) calls delete_user_person_speech_samples(uid, person_id) after deleting the person doc, to clean up the GCS audio blobs. The MCP delete_person path only deletes the Firestore doc, which would orphan any uploaded speech samples for that person. Worth adding the same cleanup call (or confirming it's handled downstream) so the two delete paths stay consistent.

Feature direction looks good — people curation (relabel misidentified speakers, dedupe, register contacts) is a natural fit for the two-way memory bank in #4862. Flagging for maintainer review given this is part of a sequence of MCP-surface expansions and touches the auth/scope surface.

@ZachL111

Copy link
Copy Markdown
Contributor Author

Thanks David, agreed on both points.

On the auth model, glad that lines up with the #8439 conclusion: the key is user-scoped, no per-key grants, and PEOPLE_WRITE_SECURITY matches the existing MEMORIES_WRITE_SECURITY pattern. Server-wide per-scope enforcement stays a separate cross-cutting PR.

On the speech-sample cleanup, you're right that the MCP delete was orphaning the GCS blobs. delete_person now calls delete_user_person_speech_samples(uid, person_id) after the Firestore delete, the same as the REST delete_person_endpoint, wrapped best-effort so a storage error cannot fail the delete (the person doc is already gone). Added a test that the cleanup runs. Pushed in 6c2992c.

@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Verified at head (6c2992c): the speech-sample cleanup raised earlier is now in delete_persondelete_user_person_speech_samples runs after the Firestore delete, best-effort with logging, mirroring the REST delete_person_endpoint. That finding is resolved in code.

What I checked: all five tools route through a single mcp_people.py orchestration (REST + SSE cannot drift); SSE raw-JSON args are type-validated before reaching the DB; outputs use clean_person (speech-sample audio URLs and speaker embeddings stripped); create_person is idempotent by name; route ordering puts by-name before /{person_id}; all referenced database.users helpers exist; auth is user-scoped via get_uid_from_mcp_api_key with no cross-user exposure.

Implementation looks clean and well-tested. Holding on formal approval since this is part of the MCP-surface expansion sequence and touches the auth/scope surface (people.write) — needs a human maintainer sign-off before merge. A gpt-5.5 pass on the scope model would be a welcome confirmation, though no vulnerability was found here.

Thanks @ZachL111 — nice work on the shared orchestration layer.

@ZachL111

Copy link
Copy Markdown
Contributor Author

Thanks David. To put the scope model in one place for the maintainer review you are gating on: API keys here are user scoped and all access. There is no per key scope enforcement anywhere in the MCP surface today, so people.write behaves exactly like memories.write and the other write tools, declared in the metadata and in the tool securitySchemes and authorized by user identity rather than by a per key grant. delete_person carries the destructive write annotation and the speech sample cleanup now mirrors the REST delete_person_endpoint, so no new privilege boundary is introduced here beyond the existing user scoped key.

A dedicated scope model pass is welcome. The broader question of server wide per key scope enforcement is a deliberate cross cutting change rather than part of this tool addition, and is the same design point raised on #8439; I am happy to support that as a separate effort if a maintainer wants to pursue it.

@kodjima33 kodjima33 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MCP feature: manage people via MCP (#4862) — approve only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature-fit-review PR touches feature direction; qualitative fit assessed needs-maintainer-review Requires human maintainer review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants