镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions apps/web/src/components/settings/ConnectionsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ import { ITEM_ROW_CLASSNAME } from "./itemRows";
import {
CloudLinkRow,
CloudRemoteEnvironmentRows,
LocalEnvironmentMcpUrlRow,
SavedBackendListRow,
} from "./SavedEnvironmentRows";
import { AddEnvironmentDialog } from "./AddEnvironmentDialog";
Expand Down Expand Up @@ -1239,6 +1240,9 @@ function ConnectionsSettingsContent({
canManageRelay={canManageRelay}
/>
) : null}
{isPrimaryAccess && accessEnvironment ? (
<LocalEnvironmentMcpUrlRow entry={accessEnvironment.entry} />
) : null}
{accessEnvironment ? (
<details className="group px-3 sm:px-4">
<summary className="flex min-h-11 cursor-pointer list-none items-center gap-2 rounded-md text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring [&::-webkit-details-marker]:hidden">
Expand Down
84 changes: 65 additions & 19 deletions apps/web/src/components/settings/SavedEnvironmentRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
hasSavedBearerRoute,
connectionStatusText,
environmentMcpUrl,
localEnvironmentMcpUrl,
type ConnectionCatalogEntry,
} from "@t3tools/client-runtime/connection";
import * as Option from "effect/Option";

Expand Down Expand Up @@ -51,10 +53,13 @@ import { hasCloudPublicConfig } from "~/cloud/publicConfig";
import { isElectron } from "~/env";
import { useT3ConnectTurnOn } from "~/cloud/useT3ConnectTurnOn";
import { useCloudLinkController } from "~/cloud/useCloudLinkController";
import { usePrimaryCloudLinkState } from "~/cloud/cloudLinkState";
import { t3ConnectMcpUrl } from "~/cloud/t3ConnectState";
import { useManagedTunnelUsage } from "~/cloud/managedRelayState";
import { useT3ConnectAccountPage } from "../clerk/T3ConnectAccountPages";
import { TunnelLimitAlert, TunnelUsageMeter } from "../cloud/TunnelUsage";
import { environmentCatalog } from "~/connection/catalog";
import { isLoopbackHostname } from "~/environments/primary/target";
import { readEnvironmentScope } from "~/state/session";
import { type EnvironmentPresentation, useRelayEnvironmentDiscovery } from "~/state/environments";
import { APP_VERSION } from "~/branding";
Expand Down Expand Up @@ -108,6 +113,28 @@ function savedBackendStatus(environment: EnvironmentPresentation): {
}
}

function useCopyMcpUrl() {
return useCopyToClipboard<{ url: string }>({
target: "MCP URL",
onCopy: ({ url }) => {
toastManager.add({
type: "success",
title: "MCP URL copied",
description: `Add it to an agent, e.g. claude mcp add --transport http t3 ${url}`,
});
},
onError: (error) => {
toastManager.add(
stackedThreadToast({
type: "error",
title: "Could not copy MCP URL",
description: error.message,
}),
);
},
});
}

export function SavedBackendListRow({
environment,
removingEnvironmentId,
Expand Down Expand Up @@ -147,25 +174,7 @@ export function SavedBackendListRow({
const copyTraceId = (traceId: string) => {
copyTraceIdToClipboard(traceId, { traceId });
};
const { copyToClipboard: copyMcpUrl } = useCopyToClipboard<{ url: string }>({
target: "MCP URL",
onCopy: ({ url }) => {
toastManager.add({
type: "success",
title: "MCP URL copied",
description: `Add it to an agent, e.g. claude mcp add --transport http t3 ${url}`,
});
},
onError: (error) => {
toastManager.add(
stackedThreadToast({
type: "error",
title: "Could not copy MCP URL",
description: error.message,
}),
);
},
});
const { copyToClipboard: copyMcpUrl } = useCopyMcpUrl();
const versionMismatch = resolveServerConfigVersionMismatch(environment.serverConfig);
const serverUpdateState = useAtomValue(serverEnvironment.updateStateAtom(environmentId));
const resumingServerUpdate =
Expand Down Expand Up @@ -434,6 +443,43 @@ export function SavedBackendListRow({
);
}

/** Copying the host's own MCP address, for hosted agents or agents on this machine. */
export function LocalEnvironmentMcpUrlRow({ entry }: { readonly entry: ConnectionCatalogEntry }) {
const cloudLink = usePrimaryCloudLinkState();
const { copyToClipboard: copyMcpUrl } = useCopyMcpUrl();
const tunnelMcpUrl = t3ConnectMcpUrl({
thisMachine: true,
tunnelHostname: cloudLink.data?.tunnelHostname,
relayEndpoint: undefined,
});
const mcpUrl = tunnelMcpUrl ?? localEnvironmentMcpUrl({ entry });
if (mcpUrl === null) return null;
const tunnelAvailable = tunnelMcpUrl !== null;
const tunnelReachability = tunnelAvailable ? cloudLink.data?.reachability : undefined;
const loopback = isLoopbackHostname(new URL(mcpUrl).hostname);
return (
<SettingsRow
{...searchableSetting("mcp-url")}
description={
tunnelReachability?.status === "reachable"
? `Tunnel connected. Give this URL to a hosted agent, such as ChatGPT: ${mcpUrl}`
: tunnelReachability?.status === "unreachable"
? `T3 Connect is unreachable right now, so a hosted agent can use this URL after the tunnel recovers: ${mcpUrl}`
: tunnelAvailable
? `Give this URL to a hosted agent, such as ChatGPT: ${mcpUrl}`
: loopback
? `Give this URL to an agent running on this machine: ${mcpUrl}`
: `Give this URL to an agent that can reach this environment: ${mcpUrl}`
}
control={
<Button size="sm" variant="outline" onClick={() => copyMcpUrl(mcpUrl, { url: mcpUrl })}>
Copy
</Button>
}
/>
);
}

function CloudLinkSwitch({
checked,
disabled,
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/components/settings/settingsSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,14 @@ export const SETTINGS_SEARCH_ITEMS = [
desktopOnly: true,
cloudOnly: true,
},
{
id: "mcp-url",
localEnvironmentOnly: true,
title: "MCP URL",
to: "/settings/access",
targetId: "connections-environment",
searchTerms: ["outside agents chatgpt claude codex hosted bot oauth server address copy"],
},
{
id: "hold-webhooks-while-offline",
localEnvironmentOnly: true,
Expand Down
22 changes: 16 additions & 6 deletions docs/user/outside-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,27 @@ client ID cannot connect.

## Get the MCP URL

In **Settings → Environments**, open a saved environment's menu and choose
**Copy MCP URL**. The URL is the environment's address followed by `/mcp`, for
example:
For the environment on this machine, open **Settings → Sharing & access** and
find **MCP URL** on its card. With T3 Connect on and its tunnel connected, this
is the environment's public address followed by `/mcp`:

```text
https://<environment-address>/mcp
```

When T3 Connect serves the environment, the copied URL is its T3 Connect
address, which agents can reach from anywhere. Otherwise it uses the route this
device is connected over, so an agent on the same device can reach it.
That is the URL a hosted agent, such as ChatGPT, needs, because it can reach
this environment from anywhere. Without a tunnel URL, the setting shows a
loopback or `https` route instead; a plain-HTTP LAN route has no MCP URL row.
Only the loopback address is limited to the machine running T3 Code.

For a saved remote environment, open **Settings → Environments**, open the
environment's menu, and choose **Copy MCP URL**.

For the local environment, the copied URL uses its T3 Connect address when the
tunnel is available, and otherwise a loopback or `https` route.

For a saved remote environment, the copied URL uses the connected route when
that route has an address, and otherwise the first saved route with one.

- **An agent on your own computers** can use `localhost` on the host, T3
Connect, or any `https` address, such as
Expand Down
6 changes: 4 additions & 2 deletions docs/user/remote-access.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,10 @@ environment of an already running T3 server.
## Connect an outside agent

Claude Code, Codex, ChatGPT and other agents T3 Code did not start can drive
threads on an environment through its MCP server. See
[outside agents](./outside-agents.md) for setup.
threads on an environment through its MCP server. On the host, **Settings →
Sharing & access** shows the MCP URL to give the agent, including its T3 Connect
address when the tunnel is on. See [outside agents](./outside-agents.md) for
setup.

## Link another environment

Expand Down
34 changes: 33 additions & 1 deletion packages/client-runtime/src/connection/presentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ import {
import {
BearerConnectionTarget,
ConnectionTransientError,
PrimaryConnectionTarget,
RelayConnectionTarget,
type SupervisorConnectionState,
} from "./model.ts";
import { environmentMcpUrl, presentConnectionState } from "./presentation.ts";
import {
environmentMcpUrl,
localEnvironmentMcpUrl,
presentConnectionState,
} from "./presentation.ts";

const TARGET = new BearerConnectionTarget({
environmentId: EnvironmentId.make("environment-1"),
Expand Down Expand Up @@ -114,6 +119,33 @@ describe("connection presentation", () => {
).toBe("https://tunnel.example.test/mcp");
});

it("offers loopback and https, but never a plain-http LAN address", () => {
const primary = new PrimaryConnectionTarget({
environmentId: TARGET.environmentId,
label: "This machine",
httpBaseUrl: "http://127.0.0.1:3773",
wsBaseUrl: "ws://127.0.0.1:3773",
});
const entry: ConnectionCatalogEntry = {
...ENTRY,
target: primary,
profile: Option.none(),
};

expect(localEnvironmentMcpUrl({ entry })).toBe("http://127.0.0.1:3773/mcp");

const lan: ConnectionCatalogEntry = {
...entry,
target: new PrimaryConnectionTarget({
environmentId: TARGET.environmentId,
label: "This machine",
httpBaseUrl: "http://192.168.4.53:3773",
wsBaseUrl: "ws://192.168.4.53:3773",
}),
};
expect(localEnvironmentMcpUrl({ entry: lan })).toBeNull();
});

it("distinguishes initial connection, reconnect, and retry errors", () => {
expect(presentConnectionState(supervisorState({ phase: "connecting", attempt: 1 }))).toEqual({
phase: "connecting",
Expand Down
17 changes: 17 additions & 0 deletions packages/client-runtime/src/connection/presentation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ServerConfig } from "@t3tools/contracts";
import { isLocalLoopbackHost } from "@t3tools/shared/hostClassification";
import * as Option from "effect/Option";

import type { ConnectionCatalogEntry } from "./catalog.ts";
Expand Down Expand Up @@ -119,6 +120,22 @@ export function environmentMcpUrl(input: {
return null;
}

/**
* The MCP address for the environment this app runs. Agents only sign in over https or
* loopback, so a plain-http LAN address is not offered.
*/
export function localEnvironmentMcpUrl(input: {
readonly entry: ConnectionCatalogEntry;
}): string | null {
const mcpUrl = environmentMcpUrl({ entry: input.entry });
return mcpUrl !== null && isAgentSignInMcpUrl(mcpUrl) ? mcpUrl : null;
}

function isAgentSignInMcpUrl(mcpUrl: string): boolean {
const url = new URL(mcpUrl);
return url.protocol === "https:" || isLocalLoopbackHost(url.hostname);
}

function mcpUrlFromBase(httpBaseUrl: string): string | null {
let url: URL;
try {
Expand Down
Loading