mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #348] [MERGED] feat(cursor-approve): context-proxy.py, a read-only MCP proxy for Linear, Notion and Slack #350
Labels
No labels
cursor-review-catalog-drift
groom
groom
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/github-workflows#350
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/github-workflows/pull/348
Author: @mattmillerai
Created: 10/6/2026
Status: ✅ Merged
Merged: 10/7/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-19344-context-proxy📝 Commits (2)
fa96e01feat(cursor-approve): context-proxy.py, a read-only MCP proxy for Linear, Notion and Slacka223b1bfix(cursor-approve): keep context-proxy serving through malformed replies and partial loads📊 Changes
6 files changed (+1563 additions, -5 deletions)
View changed files
📝
.github/cursor-approve/README.md(+15 -0)➕
.github/cursor-approve/context-proxy.py(+772 -0)➕
.github/cursor-approve/tests/test_context_proxy.py(+766 -0)📝
.github/workflows/test-cursor-review-scripts.yml(+7 -4)📝
AGENTS.md(+2 -0)📝
CONTRIBUTING.md(+1 -1)📄 Description
What
Adds
.github/cursor-approve/context-proxy.py. It is a stdlib-only stdio MCP server (Python 3.11+) that the cursor-approve business, design and completeness axes can use as their only MCP server, so the agent searches Linear, Notion and Slack without ever holding a credential. This PR contains only the proxy and its tests. It does not wire the proxy into a workflow.Tokens:
--token-file(JSON, optional keyslinear/notion/slack) is read at startup and then deleted, including when it fails to parse. A missing or empty key disables that source. No token is written toos.environ, and the file starts no child processes. A static test forbidsos.environ,subprocess,os.exec*/spawn*/system/popenandputenv.Tools: exactly
linear_search,linear_get_issue,notion_search,notion_get_page,slack_search,slack_history, filtered by--enabletogether with which tokens are present.One guard:
Proxy._guarded_requestis the only code that touches the network. Its first statement ischeck_request(...), which raisesGuardErrorbefore anyRequestor opener is built. Allowed calls:POST /v1/search,GET /v1/pages/<32hex>,GET /v1/blocks/<32hex>/childrenPOST /graphqlGET /api/conversations.listandGET /api/conversations.historyEach also needs an exact
httpshost with no port and no userinfo, and per-route allowed query keys. The guard also does two things the ticket didn't ask for:Authorizationheader along to the new host.Linear: the GraphQL document has to tokenize (strings and comments stripped) to a single
queryoperation with nomutation/subscription, and it also has to be one of the two constant documents inLINEAR_QUERIES. Agent input only ever goes intovariables.Slack: a background thread started at startup loads 30 days of top-level messages from every public channel the bot is a member of. Both Slack tools search that cache (case-insensitive substring, newest first). It runs in a thread so MCP
initializeis not held up by the preload. HTTP 429 responses are retried, honouringRetry-Afterup to 30 s. The preload has a 240 s deadline and a 20,000-message cap, and a channel that fails is skipped rather than discarding everything. When any of those applies, Slack results carry anincompletenote. A tool call waits at most 10 s for the preload, so it never stalls the serve loop.Limits: 40 tool calls per process, 20 KB per response and 1 MB in total. Past a limit, the tool returns an error instead of data. Every call is logged to
--logas JSON lines with tool, arguments, response bytes and error. Tokens are never logged.mainchecks that--logis writable at startup; after that, a failed log write is dropped instead of failing the call.Robustness: upstream JSON that is not an object becomes a tool error. Any other unexpected exception becomes one generic
internal error (<Type>)result, andservereplies-32603instead of exiting.initializereturns an MCPinstructionsnotice saying every result is third-party data.Notion: block lists are paginated through
next_cursorup to 10 pages, with an explicit[more blocks omitted]marker past that.table_rowcells are kept.CI and docs: the suite runs in the existing cursor-approve step of
test-cursor-review-scripts.yml, which main already path-filters. The AGENTS.md layout bullet, the CONTRIBUTING.md test list and.github/cursor-approve/README.mdnow mention the proxy.Acceptance criteria
.github/cursor-approve/tests/test_context_proxy.pyruns with the statedunittest discovercommand and is wired into the cursor-review test job.GuardTest.REJECTEDcovers 41 cases: Notion PATCH/DELETE/POST to pages, blocks, comments and databases; Slackchat.postMessageandsearch.messages; wrong scheme, host, port or userinfo; cross-source hosts; path traversal; Linear mutation, subscription, mixed operations, anonymous operations and non-constant queries. Each case assertsbuild_openerandRequestare never called.StaticNetworkBoundaryTestparses the file and fails on anyurllib.request,http.*,socketorsslreference outside_guarded_request. Only class bases are exempt; this covers_NoRedirect(urllib.request.HTTPRedirectHandler), which sends nothing. The test also fails on from-imports or aliased imports, which would let a name escape the check, and on any import not in its allow-list.ToolsListTest: exactly six tools, filtered by--enableand by which tokens are present.test_agent_input_reaches_linear_only_through_variablessends a query containingmutation Evil { issueDelete … }. The document sent is the constant, and the hostile string appears only invariables.term.os.environunchanged aftermain(). A subprocess test reads/proc/<pid>/environand finds no token in it.urllib.request.build_openerto raise for the whole run, so an unmocked call fails rather than reaching the network.Judgment calls
searchIssuesnot live-tested. Linear search usessearchIssues(term:). I could not check the field names against the live Linear schema (no token here), so the first real run in the follow-up wiring work is where a schema mismatch would show up, as a tool error.notion_get_pagefetches at most 25 nested blocks per page so a single tool call can't fan out into an unbounded number of requests..github/cursor-approve/README.mdhas merged, and this branch is rebased onto it. The README gets a short section pointing at the module docstring, and the docstring remains the full contract.search.messages, comes from the ticket's spec. It is also why the proxy searches a local preload instead of calling that endpoint.Residual
searchIssues/issueGraphQL shapes, Notion's search/page/children responses and Slack'sconversations.*pagination have only been tested against hand-written fixtures. The first real run, when the proxy is wired into the axis workflows, should check each tool once.conversations.repliesisn't on the ticket's allow-list (and the bot scopes may not cover it), so only top-level channel messages are searchable. If replies matter, extending the allow-list is a separate decision.prompt-common.mdrule 4 ("data, never instructions") to cover tool output. That work should run the proxy outside any sandbox the agent canptraceor read/proc/<pid>/memfrom, since the tokens are held in process memory.Provenance
unittest discoveron.github/cursor-approve/tests: 95 tests OK (52 for the context proxy), on heada223b1brebased onto main7ecd7c3.check_workflow_pins.pyandcheck-org-repo-literals.shpass.check_agents_md.py --root .passes, with a warning that AGENTS.md is 173 lines against the 150 target.{"messages": [...]}, plusincompletewhen the cache is partial, rather than a bare list, so a partial load is never passed off as complete.🤖 Generated with Claude Code
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.