Skip to content
Draft
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
5 changes: 4 additions & 1 deletion plugin/chatgpt-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ not-found-or-not-authorized problem rather than an empty success.
Inside the server a `DaemonBridge` reads through `@tracedecay/sdk` (the
workspace's generated operation contracts, never hand-edited):

- MCP-transport operations (`project_list`, `search`, `node`, `impact`,
- Registry operations (`project_list`, `project_context`) use a projectless
`RegistryClient` over a profile-scoped `tracedecay serve` session; it takes
no base URL, project id, or token.
- MCP-transport project operations (`search`, `node`, `impact`,
`context`, `status`, `find_exact_symbol`) go through an `McpToolAdapter`
that spawns `tracedecay serve` as an MCP client.
- HTTP operations (`code_symbol_search`, `code_callers`, `code_callees`,
Expand Down
17 changes: 5 additions & 12 deletions plugin/chatgpt-extension/src/server/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
} from "@tracedecay/sdk";
import type { DaemonState, Failure, ProjectRef } from "../shared/view.js";
import { httpBaseUrl, probeDaemon, readDaemonAuthority, type AuthorityLookup, type DaemonAuthorityRecord } from "./authority.js";
import { RegistryClient } from "./registry-client.js";
import { classifyCode, DaemonFailure, ServeSession, toFailure } from "./serve-client.js";

export type BridgeOptions = {
Expand Down Expand Up @@ -90,29 +91,21 @@ export class DaemonBridge {
return this.#registry;
}

#registryClient(): TraceDecayClient {
return createClient({
baseUrl: "http://127.0.0.1",
projectId: "registry",
token: "unused-registry-reads-use-mcp",
mcp: {
callTool: (toolName, request, options) =>
this.#registrySession().callJson(toolName, asRecord(request), options.signal),
},
});
#registryClient(): RegistryClient {
return new RegistryClient((toolName, request, signal) => this.#registrySession().callJson(toolName, request, signal));
}

async listProjects(signal?: AbortSignal): Promise<readonly ProjectRef[]> {
await this.#requireAuthority();
const client = this.#registryClient();
const listed = await runSdk(() => client.operations.project_list({ limit: 10 }, optional(signal)));
const listed = await runSdk(() => client.projectList({ limit: 10 }, signal));
return listed.projects.map(projectRef);
}

async resolveProject(projectId: string, signal?: AbortSignal): Promise<ProjectRef> {
await this.#requireAuthority();
const client = this.#registryClient();
const context = await runSdk(() => client.operations.project_context({ project_selector: { project_id: projectId } }, optional(signal)));
const context = await runSdk(() => client.projectContext({ project_selector: { project_id: projectId } }, signal));
if (context.status === "not_found") {
const cached = this.#projects.get(projectId);
this.#projects.delete(projectId);
Expand Down
72 changes: 72 additions & 0 deletions plugin/chatgpt-extension/src/server/registry-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
OPERATIONS,
TraceDecayAbortError,
TraceDecayMalformedResponseError,
TraceDecayTransportError,
type OperationApplicationProjectContextRequest,
type OperationApplicationProjectContextResult,
type OperationApplicationProjectListRequest,
type OperationApplicationProjectListResult,
} from "@tracedecay/sdk";

/** Executes a generated MCP tool binding against a projectless daemon session. */
export type RegistryToolCaller = (toolName: string, request: Record<string, unknown>, signal?: AbortSignal) => Promise<unknown>;

type RegistryDescriptor<Req, Res> = {
readonly operation: string;
readonly transport: { readonly kind: string; readonly toolName?: string };
readonly decodeRequest: (value: unknown) => Req;
readonly decodeResult: (value: unknown) => Res;
};

function registryDescriptor<Req, Res>(operation: string): RegistryDescriptor<Req, Res> {
const descriptor = OPERATIONS.find((candidate) => candidate.operation === operation);
if (descriptor === undefined || descriptor.transport.kind !== "mcp_tool") {
throw new TypeError(`${operation} is not an MCP-backed operation in the canonical catalog`);
}
return descriptor as unknown as RegistryDescriptor<Req, Res>;
}

const PROJECT_LIST = registryDescriptor<OperationApplicationProjectListRequest, OperationApplicationProjectListResult>("project_list");
const PROJECT_CONTEXT = registryDescriptor<OperationApplicationProjectContextRequest, OperationApplicationProjectContextResult>("project_context");

/**
* Projectless registry capability. It has no base URL, project id, or token:
* registry reads are admitted by the profile-scoped serve session alone, with
* operation identity and request/result contracts taken from the generated
* catalog.
*/
export class RegistryClient {
readonly #call: RegistryToolCaller;

constructor(call: RegistryToolCaller) {
this.#call = call;
}

projectList(request: OperationApplicationProjectListRequest, signal?: AbortSignal): Promise<OperationApplicationProjectListResult> {
return this.#invoke(PROJECT_LIST, request, signal);
}

projectContext(request: OperationApplicationProjectContextRequest, signal?: AbortSignal): Promise<OperationApplicationProjectContextResult> {
return this.#invoke(PROJECT_CONTEXT, request, signal);
}

async #invoke<Req, Res>(descriptor: RegistryDescriptor<Req, Res>, request: Req, signal?: AbortSignal): Promise<Res> {
const toolName = descriptor.transport.toolName as string;
const decoded = descriptor.decodeRequest(request) as Record<string, unknown>;
let response: unknown;
try {
response = await this.#call(toolName, decoded, signal);
} catch (cause) {
if (signal?.aborted) throw new TraceDecayAbortError(signal.reason ?? cause);
throw new TraceDecayTransportError(`MCP tool ${toolName} failed`, cause);
}
try {
return descriptor.decodeResult(response);
} catch (cause) {
throw new TraceDecayMalformedResponseError(`MCP tool ${toolName} returned an invalid ${descriptor.operation} result`, {
payload: { response, cause },
});
}
}
}