Skip to content

Generic remote connectors (multi-session, routing, secrets) - #155

Merged
kentcdodds merged 7 commits into
mainfrom
cursor/generic-remote-connectors-3cce
Apr 12, 2026
Merged

kentcdodds merged 7 commits into
mainfrom
cursor/generic-remote-connectors-3cce

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Apr 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

Generalizes the home connector into remote connectors: stable session keys, /connectors/:kind/:instanceId plus legacy /home/connectors/:id, McpCallerContext.remoteConnectors, per-connector secrets (REMOTE_CONNECTOR_SECRETS), merged synthesized tool domains, meta + search status hints, and architecture docs.

Follow-ups in this branch

  • Preview workflow: removed duplicate CLOUDFLARE_ACCOUNT_ID from secret overrides (Wrangler binding conflict / API 10053).
  • PR review: remoteConnectorStatuses in search structured content; stricter ref/env validation; parallel domain synthesis and meta status; session hello canonicalization; tests for generic /connectors/home/... and remoteConnectors: [].
  • Latest commit: whitespace-only connectorKind falls back to home in session hello; loadDownRemoteConnectorStatuses uses Promise.all; capability name sanitization trims leading/trailing underscores; route parsing decodes without redundant pre-trim.

Test plan

  • npm run typecheck
  • npm run test (worker)
  • npm run lint
  • npm run format
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Support for multiple remote connector kinds/instances; added a capability to list/report remote connector statuses and per-connector status fields in search results.
    • Optional per-connector secret overrides via REMOTE_CONNECTOR_SECRETS; connector hello now includes connectorKind.
  • Refactor

    • Unified routing, session keys, handshake, client/session handling, and capability synthesis to support arbitrary connector kinds/instances; registry merges synthesized remote domains.
  • Documentation

    • Added comprehensive docs for remote connectors, routes, secrets, and troubleshooting.
  • Tests

    • Added tests for routing, normalization, secret resolution, and session-key utilities.
  • Bug Fixes

    • Minor CI workflow and test assertion adjustments.

- Add /connectors/:kind/:instanceId alongside /home/connectors/:id; stable
  DO keys via connectorSessionKey (home ids unchanged).
- Pass X-Kody-Connector-Session-Key into the session DO; validate hello kind
  and id against ingress; optional connectorKind on hello from home-connector.
- Extend McpCallerContext with remoteConnectors; normalize refs for registry,
  Home MCP bridge, meta, and search.
- Merge multiple synthesized remote-tool domains; keep legacy home_* names
  for single home:default; use remote:<kind>:<id> domains otherwise.
- Add REMOTE_CONNECTOR_SECRETS JSON map with HOME_CONNECTOR_SHARED_SECRET
  fallback for home; meta_list_remote_connector_status capability.
- Search structured content gains remoteConnectorStatuses; homeConnectorStatus
  includes connectorKind when present.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Apr 11, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Generalizes the home connector into a remote-connector model: adds normalization, routing, secret resolution, session keys, schemas, capability synthesis, status tooling, tests, and docs; updates routing/session handling and propagates connector kind+instanceId across worker/home/tooling codepaths.

Changes

Cohort / File(s) Summary
Workflow
\.github/workflows/preview.yml
Removed Cloudflare-specific preview secret override; simplified conditional if: expressions in cleanup job (logical layout change).
Shared schema & context
packages/shared/src/chat.ts, packages/shared/src/remote-connectors.ts, packages/worker/src/mcp/context.ts, packages/worker/src/mcp/context.node.test.ts
Added remoteConnectorRefSchema/RemoteConnectorRef, normalizeRemoteConnectorRefs, and optional remoteConnectors in MCP caller context (defaults to null); adjusted context parsing tests.
Env / Secrets
packages/worker/src/env-schema.ts, packages/worker/src/remote-connector/resolve-remote-connector-secret.ts, packages/worker/src/remote-connector/resolve-remote-connector-secret.node.test.ts
Added REMOTE_CONNECTOR_SECRETS env schema and parsing; implemented resolveRemoteConnectorSharedSecret with JSON map lookup and home fallback; tests added.
Routing / Session keys
packages/worker/src/remote-connector/connector-session-key.ts, packages/worker/src/remote-connector/connector-session-key.node.test.ts, packages/worker/src/index.ts
Introduced connectorSessionKey, parseConnectorRoutePath, connectorIngressPath; replaced ad-hoc /home/connectors handling with route parsing, session-key-derived DO lookup, and header injection; tests added.
Home → Remote generalization
packages/worker/src/home/...
client-transport.ts, client.ts, mcp.ts, session.ts, status.ts, types.ts, utils.ts
Transport/client/session/status/types updated to accept kind + instanceId; session validation uses ingress session key; persisted state and snapshots include connectorKind; shared-secret resolution switched to per-connector lookup; compatibility wrappers added.
Capability synthesis & registry
packages/worker/src/mcp/capabilities/home/index.ts, packages/worker/src/mcp/capabilities/domain-metadata.ts, packages/worker/src/mcp/capabilities/registry.ts
Replaced home-only synthesis with synthesizeRemoteToolDomain per ref; CapabilityDomain broadened to string; registry now synthesizes domains for each normalized remote connector ref concurrently and aggregates results.
Meta capabilities & tools
packages/worker/src/mcp/capabilities/meta/*, packages/worker/src/mcp/tools/*, packages/worker/src/mcp/tools/search-format.ts
Added meta_list_remote_connector_status capability and handler; adapted meta_get_home_connector_status to use normalized refs; search now aggregates remote connector statuses and updated structured search schema/serialization.
Remote connector IDs & capability prefixes
packages/worker/src/remote-connector/remote-domain-id.ts
Added remoteConnectorDomainId and remoteConnectorCapabilityPrefix utilities for stable domain IDs and capability naming with home-compat rules.
Home-connector client (external)
packages/home-connector/src/transport/worker-connector.ts
connector.hello message now includes connectorKind: 'home' for compatibility.
Tests / small edits
packages/worker/client/mcp-apps/kody-ui-utils.ts, packages/worker/src/mcp-auth.workers.test.ts, packages/worker/src/remote-connector/*, packages/worker/src/remote-connectors-shared.node.test.ts
Minor refactors (predicate formatting), relaxed assertions to partial matches, and multiple new unit tests covering normalization, parsing, routing, and secret resolution.
Docs
docs/contributing/architecture/remote-connectors.md, docs/contributing/*, docs/use/troubleshooting.md
New remote-connectors architecture and env docs; updated routing, capability, and troubleshooting docs explaining URLs, handshake, secrets, and registry behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Worker as Worker/Router
    participant Session as HOME_CONNECTOR_SESSION<br/>DurableObject
    participant Connector as Remote Connector

    Client->>Worker: HTTP/WS request /connectors/:kind/:instanceId/...
    Worker->>Worker: parseConnectorRoutePath(pathname)
    Worker->>Worker: sessionKey = connectorSessionKey(kind, instanceId)
    Worker->>Session: resolve by name sessionKey
    Worker->>Session: forward request (add header X-Kody-Connector-Session-Key)
    Session->>Connector: deliver request (upgrade / RPC)
    Connector->>Connector: validate ingressSessionKey matches header
    alt keys match
        Connector->>Worker: respond with RPC/snapshot result
        Worker-->>Client: proxied response
    else mismatch
        Connector->>Worker: send server.error and close (4003)
        Worker-->>Client: error response
    end
Loading
sequenceDiagram
    participant App as MCP App
    participant Normalizer as Remote Connector Ref Normalizer
    participant Registry as Capability Registry
    participant Synth as Remote Tool Domain Synthesizer
    participant Client as Remote MCP Client
    participant Connector as Remote Connector

    App->>Normalizer: normalizeRemoteConnectorRefs(callerContext)
    Normalizer-->>App: [{kind,instanceId}, ...]
    App->>Registry: getCapabilityRegistryForContext(...)
    Registry->>Synth: synthesizeRemoteToolDomain(env, ref, allRefs) (concurrent)
    Synth->>Client: createRemoteConnectorMcpClient(env, ref.kind, ref.instanceId)
    Client->>Connector: fetch snapshot (MCP)
    Connector-->>Client: snapshot
    Synth->>Registry: {domain, bindings}
    Registry-->>App: combined registry with synthesized domains
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 Hops of joy through connector kinds,

Keys and secrets tucked in lines,
Kinds and instances snugly named,
Domains awaken, tools reclaimed,
Rabbit cheers — connections framed!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Generic remote connectors (multi-session, routing, secrets)' accurately summarizes the main changes: generalizing home connector functionality to support multiple remote connectors with different kinds/instances, new routing patterns, and environment-based secrets configuration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/generic-remote-connectors-3cce

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kentcdodds
kentcdodds marked this pull request as ready for review April 11, 2026 23:40
@github-actions

github-actions Bot commented Apr 11, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-155.kentcdodds.workers.dev

Worker: kody-pr-155
D1: kody-pr-155-db
KV: kody-pr-155-oauth-kv

Mocks:

- Add architecture/remote-connectors.md (URLs, hello, JSON-RPC, MCP context)
- Link from home-connector, request-lifecycle, architecture index, env vars,
  adding-capabilities; refresh troubleshooting meta tool hints

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/worker/src/mcp/tools/search.ts (1)

819-834: ⚠️ Potential issue | 🟠 Major

remoteConnectorStatuses never makes it into the final structured response.

Line 819 rebuilds SearchResultStructuredContent, but it only copies homeConnectorStatus and omits trimmedPayload.remoteConnectorStatuses. As written, callers will never see the new remote status payload even when Lines 712-776 populate it.

Proposed fix
 			const result: SearchResultStructuredContent = {
 				offline: trimmedPayload.offline,
 				warnings: trimmedPayload.warnings,
 				...(trimmedPayload.memories
 					? {
 							memories: trimmedPayload.memories,
 						}
 					: {}),
 				...(trimmedPayload.homeConnectorStatus
 					? { homeConnectorStatus: trimmedPayload.homeConnectorStatus }
 					: {}),
+				...(trimmedPayload.remoteConnectorStatuses
+					? {
+							remoteConnectorStatuses:
+								trimmedPayload.remoteConnectorStatuses,
+						}
+					: {}),
 				matches: toSlimStructuredMatches({
 					matches: trimmedPayload.matches,
 					baseUrl,
 				}),
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/tools/search.ts` around lines 819 - 834, The
constructed SearchResultStructuredContent object omits
trimmedPayload.remoteConnectorStatuses so callers never receive
remoteConnectorStatuses; update the object construction in the block building
SearchResultStructuredContent to include remoteConnectorStatuses when present
(similar to the existing conditional for homeConnectorStatus), i.e. check
trimmedPayload.remoteConnectorStatuses and add { remoteConnectorStatuses:
trimmedPayload.remoteConnectorStatuses } into the spread so the final response
contains the remoteConnectorStatuses alongside matches produced by
toSlimStructuredMatches.
🧹 Nitpick comments (4)
packages/worker/src/mcp/context.node.test.ts (1)

32-43: Re-add explicit assertions for connector defaults in parse test.

toMatchObject is reasonable, but this test now misses regressions where parsed homeConnectorId/remoteConnectors defaults drift.

✅ Minimal test-hardening diff
 test('parseMcpCallerContext validates caller context shape', () => {
-	expect(
-		parseMcpCallerContext({
+	const parsed = parseMcpCallerContext({
 			baseUrl: 'https://example.com',
 			user: {
 				userId: '123',
 				email: 'user@example.com',
 				displayName: 'user',
 			},
 			storageContext: {
 				sessionId: 'session-123',
 				appId: 'app-123',
 			},
-		}),
-	).toMatchObject({
+		})
+	expect(parsed).toMatchObject({
 		baseUrl: 'https://example.com',
 		user: {
 			userId: '123',
 			email: 'user@example.com',
 			displayName: 'user',
 		},
 		storageContext: {
 			sessionId: 'session-123',
 			appId: 'app-123',
 		},
 	})
+	expect(parsed).toMatchObject({
+		homeConnectorId: null,
+		remoteConnectors: null,
+	})
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/context.node.test.ts` around lines 32 - 43, The test
uses toMatchObject and therefore doesn't assert connector default values; update
the failing test in context.node.test.ts to re-add explicit assertions that the
parsed context's homeConnectorId and remoteConnectors have the expected default
values (e.g., expect(parsed.homeConnectorId).toBe(...) and
expect(parsed.remoteConnectors).toEqual(...)) after calling the parser (the
function under test, e.g., parse or parseContext), so regressions in defaults
for homeConnectorId and remoteConnectors are caught.
packages/worker/src/remote-connector/remote-connectors-shared.node.test.ts (1)

4-26: Add an explicit remoteConnectors: [] regression case.

The compatibility rule here hinges on treating remoteConnectors: undefined differently from an explicitly empty array. Right now the suite only covers undefined and a populated array, so an implementation that accidentally falls back to homeConnectorId for [] would still pass.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/remote-connector/remote-connectors-shared.node.test.ts`
around lines 4 - 26, Add a regression test for normalizeRemoteConnectorRefs to
assert that when remoteConnectors is explicitly an empty array it does NOT fall
back to homeConnectorId; call normalizeRemoteConnectorRefs with homeConnectorId
set (e.g., 'living-room') and remoteConnectors: [] and expect an empty array
result ([]), similarly to the existing tests for undefined and populated arrays
so the function's handling of an explicit empty array is validated.
packages/worker/src/mcp/capabilities/meta/meta-list-remote-connector-status.ts (1)

43-60: Fetch connector statuses concurrently.

Each getRemoteConnectorStatus() call is independent, so this loop makes latency grow linearly with connector count. Promise.all() keeps this troubleshooting capability responsive when several remotes are attached.

♻️ Suggested change
 		async handler(_args, ctx) {
 			const refs = normalizeRemoteConnectorRefs(ctx.callerContext)
-			const connectors = []
-			for (const ref of refs) {
-				const s = await getRemoteConnectorStatus(ctx.env, ref)
-				connectors.push({
+			const connectors = await Promise.all(
+				refs.map(async (ref) => {
+					const s = await getRemoteConnectorStatus(ctx.env, ref)
+					return {
 						connector_kind: s.connectorKind,
 						connector_instance_id: s.connectorId ?? ref.instanceId,
 						status: s.state,
 						connected: s.connected,
 						connected_at: s.connectedAt,
 						last_seen_at: s.lastSeenAt,
 						tool_count: s.toolCount,
 						message: s.message,
 						error: s.error,
-				})
-			}
+					}
+				}),
+			)
 			return { connectors }
 		},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/worker/src/mcp/capabilities/meta/meta-list-remote-connector-status.ts`
around lines 43 - 60, The handler currently awaits getRemoteConnectorStatus
sequentially inside the for loop causing linear latency; change it to fetch all
statuses concurrently using Promise.all over refs (from
normalizeRemoteConnectorRefs(ctx.callerContext)) and then map results into the
connectors array (preserve keys connector_kind, connector_instance_id, status,
connected, connected_at, last_seen_at, tool_count, message, error). Ensure
connector_instance_id falls back to ref.instanceId when s.connectorId is
nullish, and keep the same return shape { connectors }.
packages/worker/src/remote-connector/connector-session-key.node.test.ts (1)

17-31: Add coverage for /connectors/home/:instanceId/....

The suite locks down custom generic routes and legacy home routes, but this PR also accepts the generic home form. A regression there would bypass the new compatibility path without failing these tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/remote-connector/connector-session-key.node.test.ts`
around lines 17 - 31, Add a test asserting that parseConnectorRoutePath
correctly handles the generic home form '/connectors/home/:instanceId/...': call
parseConnectorRoutePath('/connectors/home/default/rpc/tools-list') (or similar)
and expect an object with kind: 'home', instanceId: 'default', and rest:
'/rpc/tools-list'; this mirrors the existing expectations for custom and legacy
home routes and ensures the generic home route is covered in
connector-session-key.node.test.ts so regressions are detected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/shared/src/chat.ts`:
- Around line 26-36: The remoteConnectorRefSchema currently allows
whitespace-only strings because minLength(1) checks raw length; update the
schema for remoteConnectorRefSchema so both fields (kind and instanceId) are
validated on their trimmed value: change the validators on kind and instanceId
in remoteConnectorRefSchema to first trim the input (or apply a
transform/refinement that calls .trim()) then enforce non-empty (e.g.,
trimmed.length >= 1 or a trimmed minLength/regex). Ensure the change is applied
to the string validators referenced as kind and instanceId so whitespace-only
values are rejected at validation time.

In `@packages/shared/src/remote-connectors.ts`:
- Around line 11-17: normalizeKind and normalizeInstanceId currently trim (and
lowercase for kind) but allow whitespace-only inputs to become empty strings;
change both functions to trim (and lower) then validate that the result is
non-empty and throw a clear Error (e.g., "invalid kind" / "invalid instanceId")
so callers fail fast instead of producing missing path segments. Apply the same
non-empty-after-trim validation to the other normalizer helpers in this file
(the functions in the 24-40 range) so all refs are rejected if they normalize to
blank.

In `@packages/worker/src/env-schema.ts`:
- Line 128: REMOTE_CONNECTOR_SECRETS is currently only validated as a non-empty
string but must be JSON that maps keys like "kind:instanceId" to credential
values; update the schema in env-schema.ts to parse and validate JSON at boot
instead of optionalNonEmptyStringSchema by replacing REMOTE_CONNECTOR_SECRETS
with a custom parser schema that: accepts an absent value, parses the string
into JSON, ensures the result is an object/dictionary, validates each key
matches the expected "^[^:]+:[^:]+$" pattern (or similar) and each value is a
non-empty string, and surfaces a descriptive validation error on parse/failure
so startup fails fast. Reference the REMOTE_CONNECTOR_SECRETS symbol when
implementing the new parse-and-validate schema.

In `@packages/worker/src/home/utils.ts`:
- Around line 45-49: The parsing currently treats a non-string connectorKind as
absent; instead, detect if 'connectorKind' exists on the input (use the presence
of the key via (value as Record<string, unknown>)['connectorKind'] or the
in-operator) and if it exists but typeof connectorKindRaw !== 'string' throw a
validation error (or return a protocol-level error) so malformed payloads are
rejected; otherwise continue to trim and lowercase connectorKind as before (keep
the connectorKindRaw -> connectorKind logic but only after confirming it's a
string).

In `@packages/worker/src/mcp/capabilities/registry.ts`:
- Around line 35-45: The loop calling synthesizeRemoteToolDomain over refs
currently lets a single rejected snapshot abort getCapabilityRegistryForContext
and drop healthy domains; change this to perform per-ref resolution (e.g., map
refs -> synthesizeRemoteToolDomain promises and use Promise.allSettled) so
failures for individual refs are caught and logged/ignored while successful
syntheses still push synthesized.domain into synthesizedDomains, and ensure the
fallback/return to staticRegistry still happens when synthesizedDomains remains
empty; look for normalizeRemoteConnectorRefs, synthesizeRemoteToolDomain, and
the surrounding getCapabilityRegistryForContext logic to implement the per-ref
error isolation.

In `@packages/worker/src/remote-connector/connector-session-key.ts`:
- Around line 20-39: parseConnectorRoutePath is using raw path segments to build
the session identity while connectorIngressPath percent-encodes kind and
instanceId, causing mismatched keys for values like "foo bar" or "team/a";
modify parseConnectorRoutePath to decodeURIComponent() the extracted kind and
instanceId (both in the '/connectors/:kind/:instanceId/...' branch and the
'/home/connectors/:instanceId/...' branch), validate that decoding succeeds and
that the decoded values are non-empty, and treat any URIError from malformed
encodings as invalid input (return null) so the session key generation matches
connectorIngressPath.

In `@packages/worker/src/remote-connector/remote-domain-id.ts`:
- Around line 3-12: The slugging logic in remoteConnectorDomainId and
remoteConnectorCapabilityPrefix collapses distinct instanceIds (e.g. "a__b" vs
"a_b") causing collisions; replace the collapsing sanitization with a reversible
or stable disambiguator: encode the raw instanceId using a safe,
filesystem/URL-friendly encoding (e.g. base64url or encodeURIComponent) or
append a short stable hash (e.g. first 8 chars of SHA-256) to the sanitized slug
so uniqueness is preserved, then use that encoded/hashed token wherever
remoteConnectorDomainId and remoteConnectorCapabilityPrefix compute ids so both
functions stay consistent and reversible.

In `@packages/worker/src/remote-connector/resolve-remote-connector-secret.ts`:
- Around line 13-29: The JSON.parse failure for REMOTE_CONNECTOR_SECRETS is
currently swallowed (catch { /* fall through to legacy */ }), causing silent
misconfiguration; update the catch to surface the error instead: in
resolve-remote-connector-secret, when parsing mapRaw fails, throw or log a clear
error that includes the original exception and the REMOTE_CONNECTOR_SECRETS
contents (and optionally the connector key `${k}:${id}`) so the caller can fail
fast rather than falling back to HOME_CONNECTOR_SHARED_SECRET; keep existing
behavior for valid parsed objects and the existing checks for fromMap but do not
silently continue on parse errors.

---

Outside diff comments:
In `@packages/worker/src/mcp/tools/search.ts`:
- Around line 819-834: The constructed SearchResultStructuredContent object
omits trimmedPayload.remoteConnectorStatuses so callers never receive
remoteConnectorStatuses; update the object construction in the block building
SearchResultStructuredContent to include remoteConnectorStatuses when present
(similar to the existing conditional for homeConnectorStatus), i.e. check
trimmedPayload.remoteConnectorStatuses and add { remoteConnectorStatuses:
trimmedPayload.remoteConnectorStatuses } into the spread so the final response
contains the remoteConnectorStatuses alongside matches produced by
toSlimStructuredMatches.

---

Nitpick comments:
In
`@packages/worker/src/mcp/capabilities/meta/meta-list-remote-connector-status.ts`:
- Around line 43-60: The handler currently awaits getRemoteConnectorStatus
sequentially inside the for loop causing linear latency; change it to fetch all
statuses concurrently using Promise.all over refs (from
normalizeRemoteConnectorRefs(ctx.callerContext)) and then map results into the
connectors array (preserve keys connector_kind, connector_instance_id, status,
connected, connected_at, last_seen_at, tool_count, message, error). Ensure
connector_instance_id falls back to ref.instanceId when s.connectorId is
nullish, and keep the same return shape { connectors }.

In `@packages/worker/src/mcp/context.node.test.ts`:
- Around line 32-43: The test uses toMatchObject and therefore doesn't assert
connector default values; update the failing test in context.node.test.ts to
re-add explicit assertions that the parsed context's homeConnectorId and
remoteConnectors have the expected default values (e.g.,
expect(parsed.homeConnectorId).toBe(...) and
expect(parsed.remoteConnectors).toEqual(...)) after calling the parser (the
function under test, e.g., parse or parseContext), so regressions in defaults
for homeConnectorId and remoteConnectors are caught.

In `@packages/worker/src/remote-connector/connector-session-key.node.test.ts`:
- Around line 17-31: Add a test asserting that parseConnectorRoutePath correctly
handles the generic home form '/connectors/home/:instanceId/...': call
parseConnectorRoutePath('/connectors/home/default/rpc/tools-list') (or similar)
and expect an object with kind: 'home', instanceId: 'default', and rest:
'/rpc/tools-list'; this mirrors the existing expectations for custom and legacy
home routes and ensures the generic home route is covered in
connector-session-key.node.test.ts so regressions are detected.

In `@packages/worker/src/remote-connector/remote-connectors-shared.node.test.ts`:
- Around line 4-26: Add a regression test for normalizeRemoteConnectorRefs to
assert that when remoteConnectors is explicitly an empty array it does NOT fall
back to homeConnectorId; call normalizeRemoteConnectorRefs with homeConnectorId
set (e.g., 'living-room') and remoteConnectors: [] and expect an empty array
result ([]), similarly to the existing tests for undefined and populated arrays
so the function's handling of an explicit empty array is validated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e850bfc1-9eca-4552-92a4-60b529c437b0

📥 Commits

Reviewing files that changed from the base of the PR and between 11280bb and 1f1f506.

📒 Files selected for processing (31)
  • .github/workflows/preview.yml
  • packages/home-connector/src/transport/worker-connector.ts
  • packages/shared/src/chat.ts
  • packages/shared/src/remote-connectors.ts
  • packages/worker/client/mcp-apps/kody-ui-utils.ts
  • packages/worker/src/env-schema.ts
  • packages/worker/src/home/client-transport.ts
  • packages/worker/src/home/client.ts
  • packages/worker/src/home/mcp.ts
  • packages/worker/src/home/session.ts
  • packages/worker/src/home/status.ts
  • packages/worker/src/home/types.ts
  • packages/worker/src/home/utils.ts
  • packages/worker/src/index.ts
  • packages/worker/src/mcp-auth.workers.test.ts
  • packages/worker/src/mcp/capabilities/domain-metadata.ts
  • packages/worker/src/mcp/capabilities/home/index.ts
  • packages/worker/src/mcp/capabilities/meta/domain.ts
  • packages/worker/src/mcp/capabilities/meta/meta-get-home-connector-status.ts
  • packages/worker/src/mcp/capabilities/meta/meta-list-remote-connector-status.ts
  • packages/worker/src/mcp/capabilities/registry.ts
  • packages/worker/src/mcp/context.node.test.ts
  • packages/worker/src/mcp/context.ts
  • packages/worker/src/mcp/tools/search-format.ts
  • packages/worker/src/mcp/tools/search.ts
  • packages/worker/src/remote-connector/connector-session-key.node.test.ts
  • packages/worker/src/remote-connector/connector-session-key.ts
  • packages/worker/src/remote-connector/remote-connectors-shared.node.test.ts
  • packages/worker/src/remote-connector/remote-domain-id.ts
  • packages/worker/src/remote-connector/resolve-remote-connector-secret.node.test.ts
  • packages/worker/src/remote-connector/resolve-remote-connector-secret.ts

Comment thread packages/shared/src/chat.ts
Comment thread packages/shared/src/remote-connectors.ts
Comment thread packages/worker/src/env-schema.ts Outdated
Comment thread packages/worker/src/home/utils.ts Outdated
Comment thread packages/worker/src/mcp/capabilities/registry.ts
Comment thread packages/worker/src/remote-connector/connector-session-key.ts
Comment on lines +3 to +12
export function remoteConnectorDomainId(ref: RemoteConnectorRef): string {
const k = ref.kind.trim().toLowerCase()
const id =
ref.instanceId
.trim()
.replaceAll(/[^\w-]+/g, '_')
.replaceAll(/_+/g, '_')
.replace(/^_|_$/g, '') || 'instance'
return `remote:${k}:${id}`
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid many-to-one slugging for connector identity.

remoteConnectorDomainId() and remoteConnectorCapabilityPrefix() both collapse distinct instance ids into the same synthesized names (a__b vs a_b, and a-b vs a_b). Once both connectors are attached, their domains/capabilities can collide even though they are different remotes. Use a reversible encoding or add a stable disambiguator.

Also applies to: 18-38

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/remote-connector/remote-domain-id.ts` around lines 3 -
12, The slugging logic in remoteConnectorDomainId and
remoteConnectorCapabilityPrefix collapses distinct instanceIds (e.g. "a__b" vs
"a_b") causing collisions; replace the collapsing sanitization with a reversible
or stable disambiguator: encode the raw instanceId using a safe,
filesystem/URL-friendly encoding (e.g. base64url or encodeURIComponent) or
append a short stable hash (e.g. first 8 chars of SHA-256) to the sanitized slug
so uniqueness is preserved, then use that encoded/hashed token wherever
remoteConnectorDomainId and remoteConnectorCapabilityPrefix compute ids so both
functions stay consistent and reversible.

Comment on lines +13 to +29
const mapRaw = env.REMOTE_CONNECTOR_SECRETS?.trim()
if (mapRaw) {
try {
const parsed = JSON.parse(mapRaw) as unknown
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
const key = `${k}:${id}`
const fromMap = (parsed as Record<string, unknown>)[key]
if (typeof fromMap === 'string' && fromMap.trim()) {
return fromMap.trim()
}
}
} catch {
// fall through to legacy
}
}
if (k === 'home') {
return env.HOME_CONNECTOR_SHARED_SECRET?.trim()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't silently ignore invalid REMOTE_CONNECTOR_SECRETS JSON.

A parse failure here is treated the same as “no map configured”. That takes every non-home connector down, and home can quietly fall back to HOME_CONNECTOR_SHARED_SECRET instead of the intended per-instance override. Please surface this as an explicit misconfiguration instead of silently continuing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/remote-connector/resolve-remote-connector-secret.ts`
around lines 13 - 29, The JSON.parse failure for REMOTE_CONNECTOR_SECRETS is
currently swallowed (catch { /* fall through to legacy */ }), causing silent
misconfiguration; update the catch to surface the error instead: in
resolve-remote-connector-secret, when parsing mapRaw fails, throw or log a clear
error that includes the original exception and the REMOTE_CONNECTOR_SECRETS
contents (and optionally the connector key `${k}:${id}`) so the caller can fail
fast rather than falling back to HOME_CONNECTOR_SHARED_SECRET; keep existing
behavior for valid parsed objects and the existing checks for fromMap but do not
silently continue on parse errors.

Comment thread packages/worker/src/home/session.ts Outdated
Comment thread packages/worker/src/remote-connector/remote-domain-id.ts
Comment thread packages/worker/src/remote-connector/connector-session-key.ts

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
docs/contributing/architecture/remote-connectors.md (1)

75-79: Clarify the bidirectional notification behavior.

The current wording conflates two distinct behaviors:

  1. How a connector should handle incoming notifications/tools/list_changed from the Worker (by re-listing tools)
  2. How the reference implementation proactively sends notifications/tools/list_changed to the Worker after server.ack

The word "responds" in "responds by sending" is confusing because it suggests responding to the incoming notification, but the sentence describes sending after server.ack instead.

📝 Suggested rewording for clarity
-The connector should handle **`notifications/tools/list_changed`** from the
-Worker by re-listing tools if it implements dynamic registration; the reference
-implementation in `packages/home-connector` responds by sending a
-`notifications/tools/list_changed` **to** the Worker after `server.ack` so the
-session refreshes its tool snapshot.
+The connector should handle **`notifications/tools/list_changed`** from the
+Worker by re-listing tools if it implements dynamic registration. The reference
+implementation in `packages/home-connector` also proactively sends a
+`notifications/tools/list_changed` **to** the Worker immediately after receiving
+`server.ack` to trigger an initial tool snapshot refresh.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/contributing/architecture/remote-connectors.md` around lines 75 - 79,
Rewrite the paragraph to separate the two behaviors: state that a connector
should handle incoming notifications/tools/list_changed from the Worker by
re-listing tools if it supports dynamic registration, and then note
independently that the reference implementation in packages/home-connector
proactively sends notifications/tools/list_changed to the Worker after
server.ack to refresh the session’s tool snapshot; remove the word “responds”
and make both actions explicitly distinct (handling incoming vs proactively
sending) and reference notifications/tools/list_changed and server.ack so
readers can locate the related logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/contributing/architecture/remote-connectors.md`:
- Around line 75-79: Rewrite the paragraph to separate the two behaviors: state
that a connector should handle incoming notifications/tools/list_changed from
the Worker by re-listing tools if it supports dynamic registration, and then
note independently that the reference implementation in packages/home-connector
proactively sends notifications/tools/list_changed to the Worker after
server.ack to refresh the session’s tool snapshot; remove the word “responds”
and make both actions explicitly distinct (handling incoming vs proactively
sending) and reference notifications/tools/list_changed and server.ack so
readers can locate the related logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: af883bcd-04d5-4f53-a903-baa248aeb1e1

📥 Commits

Reviewing files that changed from the base of the PR and between 1f1f506 and 599267d.

📒 Files selected for processing (7)
  • docs/contributing/adding-capabilities.md
  • docs/contributing/architecture/home-connector.md
  • docs/contributing/architecture/index.md
  • docs/contributing/architecture/remote-connectors.md
  • docs/contributing/architecture/request-lifecycle.md
  • docs/contributing/environment-variables.md
  • docs/use/troubleshooting.md
✅ Files skipped from review due to trivial changes (5)
  • docs/contributing/architecture/index.md
  • docs/contributing/architecture/home-connector.md
  • docs/contributing/adding-capabilities.md
  • docs/use/troubleshooting.md
  • docs/contributing/environment-variables.md

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/home/session.ts`:
- Around line 228-230: Canonicalize connectorKind and connectorId before any
auth, persistence, or ACKs: compute declaredKind by trimming
message.connectorKind (defaulting to 'home' if null/empty after trim) then
lowercasing it, and compute canonicalInstanceId by trimming message.connectorId;
use these (not raw message fields) when calling connectorSessionKey, when
persisting/echoing identities, and in the other blocks that reference
connectorKind/connectorId (the same logic applied around the later uses in this
file). Ensure whitespace-only connectorKind falls back to 'home' and replace raw
message.connectorId with canonicalInstanceId everywhere it's stored or echoed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d61c5b6a-a844-4a6b-ba90-abcabff5c801

📥 Commits

Reviewing files that changed from the base of the PR and between 599267d and 71cf909.

📒 Files selected for processing (3)
  • packages/worker/src/home/session.ts
  • packages/worker/src/remote-connector/connector-session-key.ts
  • packages/worker/src/remote-connector/remote-domain-id.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/worker/src/remote-connector/connector-session-key.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/worker/src/remote-connector/remote-domain-id.ts

Comment thread packages/worker/src/home/session.ts Outdated
Comment thread packages/worker/src/remote-connector/connector-session-key.ts
Comment thread packages/worker/src/home/client-transport.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/mcp/capabilities/home/index.ts Outdated
Comment thread packages/worker/src/mcp/capabilities/home/index.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
return id
}
return `${k}:${id}`
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Session key collision between home-colon instanceIds and non-home kinds

Low Severity

connectorSessionKey('home', 'foo:bar') returns 'home:foo:bar', and connectorSessionKey('home:foo', 'bar') also returns 'home:foo:bar'. Two logically distinct connector references — one with kind='home' and a colon-containing instanceId, the other with a colon-containing kind — resolve to the same Durable Object session key, causing them to share state, WebSocket connections, and tool snapshots.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 141ee10. Configure here.

Comment thread packages/worker/src/mcp/capabilities/registry.ts
…low-ups

- Stop writing CLOUDFLARE_ACCOUNT_ID into preview secret overrides; preview
  wrangler already injects it as a var, and bulk secret upload rejected duplicate
  binding names (Cloudflare API 10053).
- Include remoteConnectorStatuses in search structuredContent after trimming.
- Validate remoteConnectorRef kind/instanceId with trimmed non-empty schemas;
  filter blank refs after normalization.
- Use Promise.allSettled for per-ref domain synthesis; parallelize meta list
  status; canonicalize connector id in session hello persistence and ack.
- Reject non-string connectorKind in hello parse when the key is present.
- Parse REMOTE_CONNECTOR_SECRETS at Worker env validation; runtime resolver
  accepts parsed object or JSON string with parse error logging.
- Tests: generic /connectors/home/... route, empty remoteConnectors regression;
  doc clarifications for list_changed and env vars.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai 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.

♻️ Duplicate comments (1)
packages/worker/src/home/session.ts (1)

228-229: ⚠️ Potential issue | 🟡 Minor

Whitespace-only connectorKind doesn't fall back to 'home'.

If message.connectorKind is " " (whitespace only), the current code produces declaredKind = "" instead of falling back to 'home'. This could cause session key mismatches downstream.

Suggested fix
-		const declaredKind = (message.connectorKind ?? 'home').trim().toLowerCase()
+		const declaredKind = message.connectorKind?.trim().toLowerCase() || 'home'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/home/session.ts` around lines 228 - 229, declaredKind
currently uses (message.connectorKind ?? 'home').trim().toLowerCase() which
turns a whitespace-only connectorKind into an empty string instead of falling
back; update the logic around declaredKind (and read message.connectorKind) to
trim first, then treat an empty result as 'home' before lowercasing — e.g.,
compute a trimmedKind = (message.connectorKind ?? '').trim(), then set
declaredKind = (trimmedKind === '' ? 'home' : trimmedKind).toLowerCase() so
whitespace-only values correctly fall back to 'home'.
🧹 Nitpick comments (3)
packages/worker/src/mcp/capabilities/home/index.ts (1)

29-32: Edge case: capability name could start/end with underscore.

The sanitization correctly replaces non-word characters, but if toolName starts or ends with special characters, the result could be prefix__toolname or prefix_toolname_. This is unlikely to cause issues but worth noting.

Optional hardening
 function createCapabilityNameFromPrefix(prefix: string, toolName: string) {
-	const safeTool = toolName.replaceAll(/[^\w]+/g, '_').replaceAll(/_+/g, '_')
+	const safeTool = toolName
+		.replaceAll(/[^\w]+/g, '_')
+		.replaceAll(/_+/g, '_')
+		.replace(/^_|_$/g, '')
 	return `${prefix}_${safeTool}`
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/capabilities/home/index.ts` around lines 29 - 32, The
sanitization in createCapabilityNameFromPrefix can produce leading/trailing
underscores when toolName starts/ends with non-word characters; update
createCapabilityNameFromPrefix to trim any leading/trailing underscores after
the current replaceAll calls (and keep the existing collapse of multiple
underscores) so the returned `${prefix}_${safeTool}` never contains accidental
double or trailing underscores; ensure you still replace internal non-word runs
with single underscores and then remove underscores at the start/end of safeTool
before concatenating.
packages/worker/src/mcp/tools/search.ts (1)

206-219: Consider parallelizing remote connector status checks.

The status checks run sequentially via for...of, which could add latency when multiple remote connectors are configured. Using Promise.all would fetch all statuses concurrently.

Suggested refactor
 export async function loadDownRemoteConnectorStatuses(input: {
 	env: Env
 	callerContext: Pick<McpCallerContext, 'homeConnectorId' | 'remoteConnectors'>
 }): Promise<Array<HomeConnectorStatus>> {
 	const refs = normalizeRemoteConnectorRefs(input.callerContext)
-	const down: Array<HomeConnectorStatus> = []
-	for (const ref of refs) {
-		const status = await getRemoteConnectorStatus(input.env, ref)
-		if (shouldIncludeRemoteConnectorStatus(status)) {
-			down.push(status)
-		}
-	}
-	return down
+	const statuses = await Promise.all(
+		refs.map((ref) => getRemoteConnectorStatus(input.env, ref)),
+	)
+	return statuses.filter(shouldIncludeRemoteConnectorStatus)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/tools/search.ts` around lines 206 - 219, The function
loadDownRemoteConnectorStatuses currently iterates refs sequentially calling
getRemoteConnectorStatus which adds latency; instead map
normalizeRemoteConnectorRefs(input.callerContext) to an array of
getRemoteConnectorStatus promises, await Promise.all to fetch statuses in
parallel, then filter the resolved statuses with
shouldIncludeRemoteConnectorStatus and return the filtered array; ensure you
still call normalizeRemoteConnectorRefs and preserve the same
HomeConnectorStatus type and error propagation behavior.
packages/worker/src/remote-connector/connector-session-key.ts (1)

33-42: Minor: redundant trim operations.

Lines 34-35 trim parts[n] before decoding, then lines 37-38 trim the decoded values again. The second trim is sufficient since decodeURIComponent doesn't add whitespace. This is harmless but slightly redundant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/remote-connector/connector-session-key.ts` around lines
33 - 42, The code in connector-session-key.ts redundantly trims the raw segments
before calling decodeSegment and then trims the decoded values again; update the
extraction to stop trimming before decodeSegment (i.e., call
decodeSegment(parts[1]) and decodeSegment(parts[2]) or otherwise remove the
earlier .trim()), keep the existing checks that ensure
decodedKind/decodedInstanceId are truthy, then continue trimming only the
decodedKind/decodedInstanceId into kind and instanceId and return { kind,
instanceId, rest } as before to eliminate the duplicate trimming while
preserving behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/worker/src/home/session.ts`:
- Around line 228-229: declaredKind currently uses (message.connectorKind ??
'home').trim().toLowerCase() which turns a whitespace-only connectorKind into an
empty string instead of falling back; update the logic around declaredKind (and
read message.connectorKind) to trim first, then treat an empty result as 'home'
before lowercasing — e.g., compute a trimmedKind = (message.connectorKind ??
'').trim(), then set declaredKind = (trimmedKind === '' ? 'home' :
trimmedKind).toLowerCase() so whitespace-only values correctly fall back to
'home'.

---

Nitpick comments:
In `@packages/worker/src/mcp/capabilities/home/index.ts`:
- Around line 29-32: The sanitization in createCapabilityNameFromPrefix can
produce leading/trailing underscores when toolName starts/ends with non-word
characters; update createCapabilityNameFromPrefix to trim any leading/trailing
underscores after the current replaceAll calls (and keep the existing collapse
of multiple underscores) so the returned `${prefix}_${safeTool}` never contains
accidental double or trailing underscores; ensure you still replace internal
non-word runs with single underscores and then remove underscores at the
start/end of safeTool before concatenating.

In `@packages/worker/src/mcp/tools/search.ts`:
- Around line 206-219: The function loadDownRemoteConnectorStatuses currently
iterates refs sequentially calling getRemoteConnectorStatus which adds latency;
instead map normalizeRemoteConnectorRefs(input.callerContext) to an array of
getRemoteConnectorStatus promises, await Promise.all to fetch statuses in
parallel, then filter the resolved statuses with
shouldIncludeRemoteConnectorStatus and return the filtered array; ensure you
still call normalizeRemoteConnectorRefs and preserve the same
HomeConnectorStatus type and error propagation behavior.

In `@packages/worker/src/remote-connector/connector-session-key.ts`:
- Around line 33-42: The code in connector-session-key.ts redundantly trims the
raw segments before calling decodeSegment and then trims the decoded values
again; update the extraction to stop trimming before decodeSegment (i.e., call
decodeSegment(parts[1]) and decodeSegment(parts[2]) or otherwise remove the
earlier .trim()), keep the existing checks that ensure
decodedKind/decodedInstanceId are truthy, then continue trimming only the
decodedKind/decodedInstanceId into kind and instanceId and return { kind,
instanceId, rest } as before to eliminate the duplicate trimming while
preserving behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 04933794-b007-4ccd-8607-97636044fb13

📥 Commits

Reviewing files that changed from the base of the PR and between 71cf909 and 38a3168.

📒 Files selected for processing (21)
  • .github/workflows/preview.yml
  • docs/contributing/adding-capabilities.md
  • docs/contributing/architecture/remote-connectors.md
  • docs/contributing/architecture/request-lifecycle.md
  • docs/contributing/environment-variables.md
  • packages/shared/src/chat.ts
  • packages/shared/src/remote-connectors.ts
  • packages/worker/src/env-schema.ts
  • packages/worker/src/home/client-transport.ts
  • packages/worker/src/home/session.ts
  • packages/worker/src/home/utils.ts
  • packages/worker/src/mcp/capabilities/home/index.ts
  • packages/worker/src/mcp/capabilities/meta/meta-list-remote-connector-status.ts
  • packages/worker/src/mcp/capabilities/registry.ts
  • packages/worker/src/mcp/context.node.test.ts
  • packages/worker/src/mcp/tools/search.ts
  • packages/worker/src/remote-connector/connector-session-key.node.test.ts
  • packages/worker/src/remote-connector/connector-session-key.ts
  • packages/worker/src/remote-connector/remote-connectors-shared.node.test.ts
  • packages/worker/src/remote-connector/resolve-remote-connector-secret.node.test.ts
  • packages/worker/src/remote-connector/resolve-remote-connector-secret.ts
✅ Files skipped from review due to trivial changes (5)
  • docs/contributing/adding-capabilities.md
  • packages/worker/src/remote-connector/remote-connectors-shared.node.test.ts
  • packages/worker/src/mcp/context.node.test.ts
  • packages/worker/src/remote-connector/resolve-remote-connector-secret.node.test.ts
  • packages/worker/src/remote-connector/connector-session-key.node.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • packages/shared/src/chat.ts
  • .github/workflows/preview.yml
  • packages/worker/src/mcp/capabilities/meta/meta-list-remote-connector-status.ts
  • packages/worker/src/home/client-transport.ts
  • packages/worker/src/env-schema.ts
  • packages/worker/src/remote-connector/resolve-remote-connector-secret.ts
  • docs/contributing/environment-variables.md
  • packages/shared/src/remote-connectors.ts
  • packages/worker/src/home/utils.ts

Comment thread packages/worker/src/mcp/tools/search.ts
Treat whitespace-only connectorKind as home in session hello; parallelize
search remote connector status fetches; trim synthesized capability name edges;
decode route segments without redundant pre-trim.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/worker/src/home/session.ts (1)

227-308: ⚠️ Potential issue | 🟠 Major

Reject blank connectorId in the hello handshake.

At Line 232, a whitespace-only message.connectorId becomes '', but the code still derives a session key, resolves secrets, persists the session, and ACKs it. That leaves an authenticated session with no usable identity, and getSnapshot() later hides it because !connectorId. Fail fast if the trimmed id is empty.

Proposed fix
 		const canonicalInstanceId = message.connectorId.trim()
+		if (!canonicalInstanceId) {
+			ws.send(
+				stringifyHomeConnectorMessage({
+					type: 'server.error',
+					message: 'Connector id is required.',
+				}),
+			)
+			ws.close(4000, 'invalid-connector-id')
+			return
+		}
 		const expectedSessionKey = connectorSessionKey(
 			declaredKind,
 			canonicalInstanceId,
 		)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/home/session.ts` around lines 227 - 308, The handleHello
flow currently accepts a blank/whitespace connectorId (message.connectorId)
after trimming (canonicalInstanceId), creating an anonymous session; add an
early guard in handleHello to reject when canonicalInstanceId === '': log/Sentry
capture a clear error (e.g., "Missing connectorId in hello"), send a
server.error message to the websocket indicating the connectorId is required,
and close the ws with an appropriate close code (e.g., 4002 or similar) without
persisting state or proceeding to connectorSessionKey or
resolveRemoteConnectorSharedSecret; ensure the check runs before computing
expectedSessionKey/expectedSecret and before persisting/session ack.
♻️ Duplicate comments (1)
packages/worker/src/remote-connector/connector-session-key.ts (1)

24-55: ⚠️ Potential issue | 🟠 Major

Don't collapse empty path segments when parsing connector routes.

At Line 24, split('/').filter(Boolean) turns malformed paths into valid identities. For example, /connectors/custom//alpha/snapshot gets parsed as kind=custom, instanceId=alpha instead of being rejected. That can route a bad URL to the wrong connector session and also mutates rest by removing empty segments. Parse fixed indexes from the raw split array and reject empty kind/instanceId segments instead.

Proposed fix
-	const parts = pathname.split('/').filter(Boolean)
+	const parts = pathname.split('/')
 	const decodeSegment = (value: string) => {
 		try {
 			return decodeURIComponent(value)
 		} catch {
 			return null
 		}
 	}
 	// /connectors/:kind/:instanceId/...
-	if (parts.length >= 3 && parts[0] === 'connectors' && parts[1] && parts[2]) {
-		const decodedKind = decodeSegment(parts[1]!)
-		const decodedInstanceId = decodeSegment(parts[2]!)
+	if (parts[0] === '' && parts[1] === 'connectors' && parts[2] && parts[3]) {
+		const decodedKind = decodeSegment(parts[2]!)
+		const decodedInstanceId = decodeSegment(parts[3]!)
 		if (!decodedKind || !decodedInstanceId) return null
 		const kind = decodedKind.trim()
 		const instanceId = decodedInstanceId.trim()
 		if (!kind || !instanceId) return null
-		const rest = parts.length > 3 ? `/${parts.slice(3).join('/')}` : ''
+		const rest = parts.length > 4 ? `/${parts.slice(4).join('/')}` : ''
 		return { kind, instanceId, rest }
 	}
 	// /home/connectors/:instanceId/...
 	if (
-		parts.length >= 3 &&
-		parts[0] === 'home' &&
-		parts[1] === 'connectors' &&
-		parts[2]
+		parts[0] === '' &&
+		parts[1] === 'home' &&
+		parts[2] === 'connectors' &&
+		parts[3]
 	) {
-		const decodedInstanceId = decodeSegment(parts[2]!)
+		const decodedInstanceId = decodeSegment(parts[3]!)
 		if (!decodedInstanceId) return null
 		const instanceId = decodedInstanceId.trim()
 		if (!instanceId) return null
-		const rest = parts.length > 3 ? `/${parts.slice(3).join('/')}` : ''
+		const rest = parts.length > 4 ? `/${parts.slice(4).join('/')}` : ''
 		return { kind: 'home', instanceId, rest }
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/remote-connector/connector-session-key.ts` around lines
24 - 55, The parsing currently collapses empty path segments because it uses
pathname.split('/').filter(Boolean); change this to use the raw split array
(const parts = pathname.split('/')) and then validate by indexing into parts
(e.g., parts[0], parts[1], parts[2], etc.) without removing empty strings; in
the connector branch that checks parts[0] === 'connectors' and the home branch
that checks parts[0] === 'home' && parts[1] === 'connectors', explicitly reject
when parts[1] or parts[2] are empty strings (after decode via decodeSegment) so
malformed paths like /connectors/custom//alpha... are returned null, and
construct rest by joining parts.slice(3).join('/') so empty segments in the tail
are preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/mcp/capabilities/home/index.ts`:
- Around line 29-35: The sanitized capability name created by
createCapabilityNameFromPrefix can collapse different tool names into the same
key, so modify the code that registers/assigns capability names (the place that
currently writes the binding and may overwrite an existing key) to detect
collisions and fail-fast: after generating capabilityName with
createCapabilityNameFromPrefix, check the target registry/map/object for an
existing entry for that capabilityName; if one exists, either throw an error
that includes both original tool names and the conflicting capabilityName, or
disambiguate deterministically (e.g., append a short counter or hash suffix) and
ensure the chosen strategy is consistently applied wherever
createCapabilityNameFromPrefix is used (including the other call sites noted).
Ensure the error includes the conflicting tool names and the generated
capabilityName so the problem is visible in CI/runtime.
- Around line 86-90: The binding currently sets kind from snapshot.connectorKind
which can differ from the synthesized ref and cause routing to the wrong
MCP/secret; update the RemoteToolCapabilityBinding construction (the binding
object that includes capabilityName, instanceId, mcpToolName) to derive kind
from ref.kind (e.g., use (ref.kind).trim().toLowerCase()) instead of
snapshot.connectorKind, and keep snapshot.connectorKind only for display/search
metadata; also ensure any downstream routing/selection logic that reads
binding.kind (the code that picks the MCP client and status target) will now use
the ref-derived kind so runtime routing remains anchored to ref.kind.

---

Outside diff comments:
In `@packages/worker/src/home/session.ts`:
- Around line 227-308: The handleHello flow currently accepts a blank/whitespace
connectorId (message.connectorId) after trimming (canonicalInstanceId), creating
an anonymous session; add an early guard in handleHello to reject when
canonicalInstanceId === '': log/Sentry capture a clear error (e.g., "Missing
connectorId in hello"), send a server.error message to the websocket indicating
the connectorId is required, and close the ws with an appropriate close code
(e.g., 4002 or similar) without persisting state or proceeding to
connectorSessionKey or resolveRemoteConnectorSharedSecret; ensure the check runs
before computing expectedSessionKey/expectedSecret and before persisting/session
ack.

---

Duplicate comments:
In `@packages/worker/src/remote-connector/connector-session-key.ts`:
- Around line 24-55: The parsing currently collapses empty path segments because
it uses pathname.split('/').filter(Boolean); change this to use the raw split
array (const parts = pathname.split('/')) and then validate by indexing into
parts (e.g., parts[0], parts[1], parts[2], etc.) without removing empty strings;
in the connector branch that checks parts[0] === 'connectors' and the home
branch that checks parts[0] === 'home' && parts[1] === 'connectors', explicitly
reject when parts[1] or parts[2] are empty strings (after decode via
decodeSegment) so malformed paths like /connectors/custom//alpha... are returned
null, and construct rest by joining parts.slice(3).join('/') so empty segments
in the tail are preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bed0f879-76a3-45f0-b3ff-e7a51b4ab3d7

📥 Commits

Reviewing files that changed from the base of the PR and between 38a3168 and e017dd3.

📒 Files selected for processing (4)
  • packages/worker/src/home/session.ts
  • packages/worker/src/mcp/capabilities/home/index.ts
  • packages/worker/src/mcp/tools/search.ts
  • packages/worker/src/remote-connector/connector-session-key.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/worker/src/mcp/tools/search.ts

Comment on lines +29 to 35
function createCapabilityNameFromPrefix(prefix: string, toolName: string) {
const safeTool = toolName
.replaceAll(/[^\w]+/g, '_')
.replaceAll(/_+/g, '_')
.replace(/^_|_$/g, '')
return `${prefix}_${safeTool}`
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fail fast on sanitized capability-name collisions.

The sanitization here can collapse distinct MCP tool names into the same capability key (foo-bar, foo_bar, foo__bar, etc.). When that happens, Line 202 silently overwrites the earlier binding and one tool becomes unreachable. Please disambiguate or throw when a generated capabilityName already exists.

Suggested guard
 	for (const tool of snapshot.tools) {
 		const { capability, binding } = createCapabilityFromTool({
 			snapshot,
 			tool,
 			ref,
 			domainId: domainIdForCapabilities,
 			capabilityPrefix,
 			domainKeywordRoots,
 		})
+		if (bindings[binding.capabilityName]) {
+			throw new Error(
+				`Duplicate synthesized capability name "${binding.capabilityName}" for ${ref.kind}:${ref.instanceId}.`,
+			)
+		}
 		capabilities.push(capability)
 		bindings[binding.capabilityName] = binding
 	}

Also applies to: 82-85, 193-202

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/capabilities/home/index.ts` around lines 29 - 35, The
sanitized capability name created by createCapabilityNameFromPrefix can collapse
different tool names into the same key, so modify the code that
registers/assigns capability names (the place that currently writes the binding
and may overwrite an existing key) to detect collisions and fail-fast: after
generating capabilityName with createCapabilityNameFromPrefix, check the target
registry/map/object for an existing entry for that capabilityName; if one
exists, either throw an error that includes both original tool names and the
conflicting capabilityName, or disambiguate deterministically (e.g., append a
short counter or hash suffix) and ensure the chosen strategy is consistently
applied wherever createCapabilityNameFromPrefix is used (including the other
call sites noted). Ensure the error includes the conflicting tool names and the
generated capabilityName so the problem is visible in CI/runtime.

Comment on lines +86 to 90
const binding: RemoteToolCapabilityBinding = {
capabilityName,
connectorId: snapshot.connectorId,
kind: (snapshot.connectorKind ?? ref.kind).trim().toLowerCase(),
instanceId: ref.instanceId,
mcpToolName: tool.name,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep runtime routing anchored to ref.kind.

binding.kind is taken from snapshot.connectorKind, but Lines 119-123 and 128-131 later use that value to select the MCP client and status target. If a connector reports a different kind than the normalized ref it was synthesized from, this capability can call/check the wrong connector/secret. Please keep binding.kind tied to ref.kind and use snapshot metadata only for display/search text. packages/worker/src/home/status.ts:140-145 routes by the supplied kind, so this changes the actual target connector.

Suggested fix
 	const binding: RemoteToolCapabilityBinding = {
 		capabilityName,
-		kind: (snapshot.connectorKind ?? ref.kind).trim().toLowerCase(),
+		kind: ref.kind.trim().toLowerCase(),
 		instanceId: ref.instanceId,
 		mcpToolName: tool.name,
 	}

Also applies to: 119-130

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/capabilities/home/index.ts` around lines 86 - 90, The
binding currently sets kind from snapshot.connectorKind which can differ from
the synthesized ref and cause routing to the wrong MCP/secret; update the
RemoteToolCapabilityBinding construction (the binding object that includes
capabilityName, instanceId, mcpToolName) to derive kind from ref.kind (e.g., use
(ref.kind).trim().toLowerCase()) instead of snapshot.connectorKind, and keep
snapshot.connectorKind only for display/search metadata; also ensure any
downstream routing/selection logic that reads binding.kind (the code that picks
the MCP client and status target) will now use the ref-derived kind so runtime
routing remains anchored to ref.kind.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Reviewed by Cursor Bugbot for commit e017dd3. Configure here.

k === 'home' &&
allRefs.length === 1 &&
allRefs[0]?.kind === 'home' &&
allRefs[0]?.instanceId.trim() === 'default'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Inconsistent normalization may cause domain/prefix mismatch

Low Severity

The isOnlyBuiltinHomeDomain check in synthesizeRemoteToolDomain uses allRefs[0]?.kind === 'home' without normalization, while the equivalent isOnlyBuiltinHome check in remoteConnectorCapabilityPrefix uses allRefs[0]?.kind.trim().toLowerCase() === 'home' with normalization. These two checks govern the same domain-id vs. capability-prefix decision for the same allRefs. If they ever disagree, capabilities would be registered with prefix home but domain remote:home:default (or vice versa), causing a silent mismatch. Currently safe because normalizeRemoteConnectorRefs always pre-normalizes, but the inconsistency is fragile.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e017dd3. Configure here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants