diff --git a/docs/contributing/architecture/remote-connectors.md b/docs/contributing/architecture/remote-connectors.md index 4eb1780b0f..a8dbd3144a 100644 --- a/docs/contributing/architecture/remote-connectors.md +++ b/docs/contributing/architecture/remote-connectors.md @@ -8,16 +8,15 @@ calls to JSON-RPC on the socket. ## URLs and session keys -`wss:///@/connectors//` +`wss:///@/connectors/` -Session key = JSON tuple `[userId, kind, instanceId]` where `kind` is lowercase -after trim. +Session key = JSON tuple `[userId, instanceId]` where `instanceId` is the +user-chosen connector name, lowercase after trim. The Worker sets header **`X-Kody-Connector-Session-Key`** on requests forwarded into the Durable Object. The connector’s **`connector.hello`** must declare a -**`connectorKind`** and **`connectorId`** (instance id) that match the session -key implied by the WebSocket URL; otherwise the session closes with a mismatch -error. +**`connectorId`** (instance id) that matches the session key implied by the +WebSocket URL; otherwise the session closes with a mismatch error. ## WebSocket message protocol @@ -31,8 +30,9 @@ All messages are **JSON objects** with a **`type`** field. (`^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$`, for example `home` or `living-room`). Connector names are globally unique per user. - **`sharedSecret`:** string — must match an enabled shared secret saved for - the connector ref in D1. - - **`connectorKind`:** non-empty string. Lowercase values are normalized. + the connector in D1. + - **`connectorKind`:** ignored by Kody (may still be sent for compatibility + with connector-kit clients). 2. **`connector.heartbeat`** - **`type`:** `"connector.heartbeat"` @@ -84,23 +84,22 @@ External connector authors only need the **WebSocket**. For capabilities to be synthesized from a connector, the MCP session must list that connector: -- **`remoteConnectors`:** optional array of `{ kind, instanceId }`, where - `instanceId` is the explicit connector name. When present (including empty), - it fully defines the set of remote connectors for that session. +- **`remoteConnectors`:** optional array of `{ instanceId }`, where `instanceId` + is the explicit connector name. When present (including empty), it fully + defines the set of remote connectors for that session. Regular authenticated MCP and chat sessions load this array from the user's saved remote connector settings. Operators can manage those settings at `/account/remote-connectors`: -- **`kind`** is protocol metadata, and **`instanceId`** is the user-chosen - connector name. Names are unique per user across all kinds because they key - `kody.remote[name]`. +- **`instanceId`** is the user-chosen connector name. Names are unique per user + and key `kody.remote[name]`. - **`enabled`** controls whether the saved shared secret can authenticate - `connector.hello` for that ref. -- **`attached`** controls whether the ref is included in normal Kody MCP/chat - caller context. + `connector.hello` for that connector. +- **`attached`** controls whether the connector is included in normal Kody + MCP/chat caller context. - **`sharedSecret`** is encrypted in D1 and authenticates only the user-scoped - connector URL for the saved ref. + connector URL for the saved name. Source: `packages/shared/src/chat.ts`, `packages/shared/src/remote-connectors.ts`, and @@ -121,15 +120,15 @@ Source: `packages/shared/src/chat.ts`, ## Connector checklist 1. **Outbound WebSocket** to your connector URL: - `wss:///@/connectors//`. -2. **Hello first** with matching **`connectorKind`** + **`connectorId`** and a - **valid `sharedSecret`** for that `kind:instanceId` pair. + `wss:///@/connectors/`. +2. **Hello first** with matching **`connectorId`** and a **valid + `sharedSecret`** for that connector name. 3. Implement **`tools/list`** and **`tools/call`** on the socket via **`connector.jsonrpc`** envelopes. 4. **Heartbeats** if the service stays connected for a long time. -5. **Operator config:** save the connector ref and shared secret from - `/account/remote-connectors`; enabled + attached refs are loaded into normal - Kody sessions so the registry merges your domain. +5. **Operator config:** save the connector and shared secret from + `/account/remote-connectors`; enabled + attached connectors are loaded into + normal Kody sessions so the registry merges your domain. ## Reference implementation diff --git a/packages/shared/src/chat.ts b/packages/shared/src/chat.ts index 54754f79ec..e52bd130dc 100644 --- a/packages/shared/src/chat.ts +++ b/packages/shared/src/chat.ts @@ -9,17 +9,6 @@ import { type InferOutput, } from 'remix/data-schema' -const remoteConnectorKindFieldSchema = createSchema( - (value, context) => { - if (typeof value !== 'string') return fail('Expected string', context.path) - const trimmed = value.trim().toLowerCase() - if (!trimmed) { - return fail('remote connector kind must not be empty', context.path) - } - return { value: trimmed } - }, -) - const remoteConnectorInstanceIdFieldSchema = createSchema( (value, context) => { if (typeof value !== 'string') return fail('Expected string', context.path) @@ -59,7 +48,6 @@ export const mcpRepoContextSchema = object({ }) const remoteConnectorRefSchema = object({ - kind: remoteConnectorKindFieldSchema, instanceId: remoteConnectorInstanceIdFieldSchema, }) diff --git a/packages/shared/src/remote-connectors.ts b/packages/shared/src/remote-connectors.ts index e66aaf4a23..7e9ecef107 100644 --- a/packages/shared/src/remote-connectors.ts +++ b/packages/shared/src/remote-connectors.ts @@ -5,17 +5,12 @@ import { buildUsernamePathPrefix } from './public-urls.ts' type McpCallerContext = InferOutput export type RemoteConnectorRef = { - kind: string instanceId: string } export const remoteConnectorNamePattern = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/ -export function normalizeRemoteConnectorKind(kind: string): string { - return kind.trim().toLowerCase() -} - export function normalizeRemoteConnectorInstanceId(instanceId: string): string { return instanceId.trim().toLowerCase() } @@ -28,20 +23,17 @@ export function isValidRemoteConnectorName(instanceId: string): boolean { export function userScopedConnectorIngressPath(input: { username: string - kind: string instanceId: string }) { - const kind = encodeURIComponent(normalizeRemoteConnectorKind(input.kind)) const instanceId = encodeURIComponent( normalizeRemoteConnectorInstanceId(input.instanceId), ) - return `${buildUsernamePathPrefix(input.username)}/connectors/${kind}/${instanceId}` + return `${buildUsernamePathPrefix(input.username)}/connectors/${instanceId}` } export function userScopedConnectorWebSocketUrl(input: { origin: string username: string - kind: string instanceId: string }) { const origin = input.origin.trim().replace(/\/+$/, '') @@ -53,8 +45,7 @@ export function normalizeRemoteConnectorRefs( ): Array { return (context.remoteConnectors ?? []) .map((ref) => ({ - kind: normalizeRemoteConnectorKind(ref.kind), instanceId: normalizeRemoteConnectorInstanceId(ref.instanceId), })) - .filter((ref) => ref.kind.length > 0 && ref.instanceId.length > 0) + .filter((ref) => ref.instanceId.length > 0) } diff --git a/packages/worker/client/routes/account-remote-connectors.tsx b/packages/worker/client/routes/account-remote-connectors.tsx index bbbdc7cc82..b5dc55d051 100644 --- a/packages/worker/client/routes/account-remote-connectors.tsx +++ b/packages/worker/client/routes/account-remote-connectors.tsx @@ -38,7 +38,6 @@ import { userScopedConnectorWebSocketUrl } from '@kody-internal/shared/remote-co type RemoteConnectorListItem = { id: string - kind: string instanceId: string connectorUrl: string enabled: boolean @@ -60,7 +59,6 @@ type AccountRemoteConnectorsPayload = { type EditorState = { id: string | null - kind: string instanceId: string enabled: boolean attached: boolean @@ -100,7 +98,6 @@ export async function accountRemoteConnectorsRouteLoader( function createEmptyEditorState(): EditorState { return { id: null, - kind: '', instanceId: '', enabled: true, attached: true, @@ -114,7 +111,6 @@ function createEditorStateFromConnector( ): EditorState { return { id: connector.id, - kind: connector.kind, instanceId: connector.instanceId, enabled: connector.enabled, attached: connector.attached, @@ -128,9 +124,9 @@ function formatTimestamp(value: string) { } function connectorLabel( - connector: Pick, + connector: Pick, ) { - return `${connector.kind}:${connector.instanceId}` + return connector.instanceId } function bytesToBase64Url(bytes: Uint8Array) { @@ -409,7 +405,6 @@ export function AccountRemoteConnectorsRoute(handle: Handle) { const enabled = formData.get('enabled') === 'on' return { ...editorState, - kind: String(formData.get('kind') ?? '').trim(), instanceId: String(formData.get('instanceId') ?? '').trim(), sharedSecret: String(formData.get('sharedSecret') ?? ''), enabled, @@ -423,11 +418,6 @@ export function AccountRemoteConnectorsRoute(handle: Handle) { if (saveState !== 'idle') return const nextEditorState = form ? readEditorStateFromForm(form) : editorState editorState = nextEditorState - if (!nextEditorState.kind.trim()) { - message = 'Connector kind is required.' - handle.update() - return - } if (!nextEditorState.instanceId.trim()) { message = 'Connector name is required.' handle.update() @@ -456,7 +446,6 @@ export function AccountRemoteConnectorsRoute(handle: Handle) { body: JSON.stringify({ action: 'save', id: nextEditorState.id, - kind: nextEditorState.kind, instanceId: nextEditorState.instanceId, enabled: nextEditorState.enabled, attached: nextEditorState.attached, @@ -565,11 +554,10 @@ export function AccountRemoteConnectorsRoute(handle: Handle) { function getEditorConnectorUrl() { if (!username || !connectorUrlOrigin) return null - if (!editorState.kind.trim() || !editorState.instanceId.trim()) return null + if (!editorState.instanceId.trim()) return null return userScopedConnectorWebSocketUrl({ origin: connectorUrlOrigin, username, - kind: editorState.kind, instanceId: editorState.instanceId, }) } @@ -609,10 +597,9 @@ export function AccountRemoteConnectorsRoute(handle: Handle) { } const isMutating = saveState !== 'idle' const isEditing = Boolean(editorState.id) - const selectedLabel = - editorState.kind && editorState.instanceId - ? `${editorState.kind}:${editorState.instanceId}` - : 'New remote connector' + const selectedLabel = editorState.instanceId + ? editorState.instanceId + : 'New remote connector' const connectorUrl = getEditorConnectorUrl() return ( @@ -747,27 +734,6 @@ export function AccountRemoteConnectorsRoute(handle: Handle) {

-