Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -573,16 +573,14 @@ jobs:

- name: ℹ️ Skip GitHub preview environment delete
if: >-
always() &&
env.PREVIEW_ENVIRONMENT_GITHUB_TOKEN == ''
always() && env.PREVIEW_ENVIRONMENT_GITHUB_TOKEN == ''
run: >
echo "Skipping GitHub preview environment delete;
PREVIEW_ENVIRONMENT_GITHUB_TOKEN is not configured."

- name: 🏷️ Delete GitHub preview environment
if: >-
always() &&
env.PREVIEW_ENVIRONMENT_GITHUB_TOKEN != ''
always() && env.PREVIEW_ENVIRONMENT_GITHUB_TOKEN != ''
uses: actions/github-script@v8.0.0
env:
EVENT_NAME: ${{ github.event_name }}
Expand Down
1 change: 1 addition & 0 deletions packages/home-connector/src/transport/worker-connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export function createWorkerConnector(input: {
)
const hello: HomeConnectorHelloMessage = {
type: 'connector.hello',
connectorKind: 'home',
connectorId: input.config.homeConnectorId,
sharedSecret: input.config.sharedSecret!,
}
Expand Down
8 changes: 8 additions & 0 deletions packages/shared/src/chat.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
array,
nullable,
number,
object,
optional,
string,
type InferOutput,
} from 'remix/data-schema'
import { minLength } from 'remix/data-schema/checks'

export const aiModeValues = ['mock', 'remote'] as const
export type AiMode = (typeof aiModeValues)[number]
Expand All @@ -21,10 +23,16 @@ export const mcpStorageContextSchema = object({
appId: optional(nullable(string())),
})

const remoteConnectorRefSchema = object({
kind: string().pipe(minLength(1)),
instanceId: string().pipe(minLength(1)),
})

export const mcpCallerContextSchema = object({
baseUrl: string(),
user: optional(nullable(mcpUserContextSchema)),
homeConnectorId: optional(nullable(string())),
remoteConnectors: optional(nullable(array(remoteConnectorRefSchema))),
storageContext: optional(nullable(mcpStorageContextSchema)),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

Expand Down
41 changes: 41 additions & 0 deletions packages/shared/src/remote-connectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { type InferOutput } from 'remix/data-schema'
import { type mcpCallerContextSchema } from './chat.ts'

type McpCallerContext = InferOutput<typeof mcpCallerContextSchema>

export type RemoteConnectorRef = {
kind: string
instanceId: string
}

function normalizeKind(kind: string): string {
return kind.trim().toLowerCase()
}

function normalizeInstanceId(instanceId: string): string {
return instanceId.trim()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Effective remote connectors for MCP execution, in order.
* When `remoteConnectors` is set (including empty), it wins.
* Otherwise `homeConnectorId` maps to `{ kind: "home", instanceId }`.
*/
export function normalizeRemoteConnectorRefs(
context: Pick<McpCallerContext, 'homeConnectorId' | 'remoteConnectors'>,
): Array<RemoteConnectorRef> {
if (
context.remoteConnectors !== undefined &&
context.remoteConnectors !== null
) {
return context.remoteConnectors.map((ref) => ({
kind: normalizeKind(ref.kind),
instanceId: normalizeInstanceId(ref.instanceId),
}))
}
const hid = context.homeConnectorId?.trim()
if (!hid) {
return []
}
return [{ kind: 'home', instanceId: hid }]
}
6 changes: 3 additions & 3 deletions packages/worker/client/mcp-apps/kody-ui-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,8 +833,7 @@ async function executeGeneratedUiShellScript(
return
}
const shouldAwaitLoad =
scriptDescriptor.executionMode === 'module' ||
scriptDescriptor.src != null
scriptDescriptor.executionMode === 'module' || scriptDescriptor.src != null
if (!scriptDescriptor.src) {
script.textContent = scriptDescriptor.textContent
if (!shouldAwaitLoad) {
Expand Down Expand Up @@ -1147,7 +1146,8 @@ async function initializeShellHostDocument() {
}
}

const isStaleRender = (renderId: number) => renderId !== latestScheduledRenderId
const isStaleRender = (renderId: number) =>
renderId !== latestScheduledRenderId

async function renderEnvelope(
envelope: RenderEnvelope | null,
Expand Down
1 change: 1 addition & 0 deletions packages/worker/src/env-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export const EnvSchema = object({
CLOUDFLARE_API_BASE_URL: optionalUrlStringSchema,
CAPABILITY_REINDEX_SECRET: optionalNonEmptyStringSchema,
HOME_CONNECTOR_SHARED_SECRET: optionalNonEmptyStringSchema,
REMOTE_CONNECTOR_SECRETS: optionalNonEmptyStringSchema,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
})

export type AppEnv = InferOutput<typeof EnvSchema>
30 changes: 17 additions & 13 deletions packages/worker/src/home/client-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type MessageExtraInfo,
} from '@modelcontextprotocol/sdk/types.js'
import { type Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
import { connectorIngressPath } from '#worker/remote-connector/connector-session-key.ts'
import { type HomeConnectorJsonRpcResponse } from './types.ts'

export class HomeConnectorClientTransport implements Transport {
Expand All @@ -15,16 +16,21 @@ export class HomeConnectorClientTransport implements Transport {
) => void

private readonly input: {
connectorId: string
kind: string
instanceId: string
baseUrl: string
}

constructor(input: { connectorId: string; baseUrl: string }) {
this.input = input
constructor(input: { kind?: string; instanceId: string; baseUrl: string }) {
this.input = {
kind: input.kind ?? 'home',
instanceId: input.instanceId,
baseUrl: input.baseUrl,
}
}

async start(): Promise<void> {
this.sessionId = this.input.connectorId
this.sessionId = `${this.input.kind}:${this.input.instanceId}`
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
}

async send(message: JSONRPCMessage): Promise<void> {
Expand All @@ -47,16 +53,14 @@ export class HomeConnectorClientTransport implements Transport {
private async forwardJsonRpc(
message: JSONRPCMessage,
): Promise<HomeConnectorJsonRpcResponse | null> {
const response = await fetch(
`${this.input.baseUrl}/home/connectors/${this.input.connectorId}/rpc/jsonrpc`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message }),
const path = `${connectorIngressPath(this.input.kind, this.input.instanceId)}/rpc/jsonrpc`
const response = await fetch(`${this.input.baseUrl}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
)
body: JSON.stringify({ message }),
})
if (!response.ok) {
throw new Error(
`Home connector bridge request failed with ${response.status}.`,
Expand Down
38 changes: 28 additions & 10 deletions packages/worker/src/home/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { type CallToolResult } from '@modelcontextprotocol/sdk/types.js'
import {
connectorIngressPath,
connectorSessionKey,
} from '#worker/remote-connector/connector-session-key.ts'
import { type HomeConnectorSnapshot, type HomeToolDescriptor } from './types.ts'

export type HomeMcpTool = HomeToolDescriptor
Expand All @@ -12,13 +16,19 @@ export type HomeMcpClient = {
getSnapshot(): Promise<HomeConnectorSnapshot | null>
}

function createSessionUrl(connectorId: string, pathname: string): string {
return `https://home-connectors/${connectorId}${pathname}`
function createSessionUrl(
kind: string,
instanceId: string,
pathname: string,
): string {
const base = connectorIngressPath(kind, instanceId)
return `https://home-connectors${base}${pathname}`
}

function getSessionStub(env: Env, connectorId: string) {
function getSessionStub(env: Env, kind: string, instanceId: string) {
const sessionKey = connectorSessionKey(kind, instanceId)
return env.HOME_CONNECTOR_SESSION.get(
env.HOME_CONNECTOR_SESSION.idFromName(connectorId),
env.HOME_CONNECTOR_SESSION.idFromName(sessionKey),
)
}

Expand All @@ -29,16 +39,17 @@ async function parseJsonResponse<T>(response: Response): Promise<T> {
return (await response.json()) as T
}

export function createHomeMcpClient(
export function createRemoteConnectorMcpClient(
env: Env,
connectorId: string,
kind: string,
instanceId: string,
): HomeMcpClient {
const stub = getSessionStub(env, connectorId)
const stub = getSessionStub(env, kind, instanceId)

return {
async listTools() {
const response = await stub.fetch(
createSessionUrl(connectorId, '/rpc/tools-list'),
createSessionUrl(kind, instanceId, '/rpc/tools-list'),
{
method: 'POST',
},
Expand All @@ -50,7 +61,7 @@ export function createHomeMcpClient(
},
async callTool(name, args) {
const response = await stub.fetch(
createSessionUrl(connectorId, '/rpc/tools-call'),
createSessionUrl(kind, instanceId, '/rpc/tools-call'),
{
method: 'POST',
headers: {
Expand All @@ -66,9 +77,16 @@ export function createHomeMcpClient(
},
async getSnapshot() {
const response = await stub.fetch(
createSessionUrl(connectorId, '/snapshot'),
createSessionUrl(kind, instanceId, '/snapshot'),
)
return parseJsonResponse<HomeConnectorSnapshot | null>(response)
},
}
}

export function createHomeMcpClient(
env: Env,
connectorId: string,
): HomeMcpClient {
return createRemoteConnectorMcpClient(env, 'home', connectorId)
}
21 changes: 14 additions & 7 deletions packages/worker/src/home/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
parseMcpCallerContext,
type McpServerProps,
} from '#worker/mcp/context.ts'
import { normalizeRemoteConnectorRefs } from '@kody-internal/shared/remote-connectors.ts'

export type HomeMcpState = {}
export type HomeMcpProps = McpServerProps
Expand All @@ -42,15 +43,21 @@ type HomeMcpBridge = {
getHomeClient(): Promise<ReturnType<typeof createHomeMcpClient>>
}

function resolveHomeBridgeInstanceId(
callerContext: McpServerProps,
): string | null {
const refs = normalizeRemoteConnectorRefs(callerContext)
const home = refs.find((r) => r.kind === 'home')
return home?.instanceId ?? null
}

async function createHomeToolErrorResult(
agent: HomeMcpBridge,
error: unknown,
): Promise<CallToolResult> {
const callerContext = agent.getCallerContext()
const status = await getHomeConnectorStatus(
agent.getEnv(),
callerContext.homeConnectorId ?? null,
)
const homeInstanceId = resolveHomeBridgeInstanceId(callerContext)
const status = await getHomeConnectorStatus(agent.getEnv(), homeInstanceId)
const fallbackMessage =
error instanceof Error ? error.message : 'Unknown home connector error.'
const message =
Expand Down Expand Up @@ -177,14 +184,14 @@ class HomeMCPBase extends McpAgent<Env, HomeMcpState, HomeMcpProps> {
}

async getHomeClient() {
const { homeConnectorId } = this.getCallerContext()
if (!homeConnectorId) {
const homeInstanceId = resolveHomeBridgeInstanceId(this.getCallerContext())
if (!homeInstanceId) {
throw new Error(
'No home connector is associated with this MCP caller context.',
)
}

return createHomeMcpClient(this.env, homeConnectorId)
return createHomeMcpClient(this.env, homeInstanceId)
}
}

Expand Down
Loading
Loading