Files
Netcatty/components/settings/tabs/ai/CopyableCodeBlock.tsx
T
0ccafbabf7 feat: optional Streamable HTTP for External MCP (#3790)
* feat: add optional Streamable HTTP for External MCP

Serve the existing catalog tools and approval path on a loopback HTTP
endpoint so clients that cannot spawn the stdio launcher, such as Codex
in WSL, can connect with a bearer token. The listener stays off until
the user enables it.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix: close HTTP sockets before stop and export the Codex bearer

Destroy tracked connections before awaiting server.close so disable does not hang on a half-open request. Prefix the Codex snippet with export so the child process receives the token.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix: advertise a real client host and return 413 intact

Wildcard binds no longer copy 127.0.0.1 into client configs; the URL stays empty until a reachable client address is set. Oversized POSTs receive a JSON-RPC 413 before the socket is closed.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix: recheck the bearer token after the HTTP body arrives

A half-open POST already passed auth before readBody. Recheck the live token before dispatch so a rotation during that wait cannot still run a tool.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix: expire idle Streamable HTTP sessions

JSON responses do not keep a socket open, so a client that never sends DELETE used to pin an MCP session until the listener stopped. Sweep sessions that have been idle for 15 minutes, and skip any session with a request still in flight.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix: return 404 when an HTTP session id is unknown

GET and DELETE with a nonempty session id that is no longer active now return 404, matching POST and the Streamable HTTP session contract. A missing session id stays 400.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix: accept bracketed IPv6 origins

Node reports URL.hostname as [::1], so an otherwise valid loopback or wildcard Origin was rejected. Compare the address with the brackets removed.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix: release rejected HTTP handshakes that never open a session

A 406 or 415 from the SDK returns normally before onsessioninitialized. Detach the approval client and close the MCP server when that happens so rejected initializes do not accumulate.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-10-20 16:39:56 +08:00

63 lines
1.7 KiB
TypeScript

import React from "react";
import { Check, Copy } from "lucide-react";
import { cn } from "../../../../lib/utils";
import { Button } from "../../../ui/button";
type CopyableCodeBlockProps = {
label?: string;
value: string;
copyKey: string;
copied: string | null;
onCopy: (key: string, text: string) => void;
copyLabel: string;
copiedLabel: string;
emptyLabel?: string;
className?: string;
};
export const CopyableCodeBlock: React.FC<CopyableCodeBlockProps> = ({
label,
value,
copyKey,
copied,
onCopy,
copyLabel,
copiedLabel,
emptyLabel,
className,
}) => {
const display = value || emptyLabel || "";
const canCopy = Boolean(value);
const isCopied = copied === copyKey;
return (
<div className={cn("space-y-1.5", className)}>
{label ? (
<div className="text-xs font-medium text-muted-foreground">{label}</div>
) : null}
<div className="group relative rounded-md border border-border/60 bg-muted/20">
<pre
className={cn(
"max-h-40 overflow-auto whitespace-pre-wrap break-all px-3 py-2.5 pr-11 font-mono text-xs leading-5",
!value && "text-muted-foreground",
)}
>
{display}
</pre>
<Button
type="button"
variant="ghost"
size="sm"
disabled={!canCopy}
className="absolute right-1.5 top-1.5 h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
onClick={() => void onCopy(copyKey, value)}
aria-label={isCopied ? copiedLabel : copyLabel}
title={isCopied ? copiedLabel : copyLabel}
>
{isCopied ? <Check size={14} className="text-emerald-500" /> : <Copy size={14} />}
</Button>
</div>
</div>
);
};