@chatroomcp/chatroom 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
File without changes
@@ -0,0 +1,26 @@
1
+ import type { McpServer, StandardSchemaWithJSON, ToolAnnotations } from "@modelcontextprotocol/server";
2
+ import type { OperationLog } from "../../operations/operation-log.js";
3
+ type PluginToolInput<Schema extends StandardSchemaWithJSON> = StandardSchemaWithJSON.InferOutput<Schema>;
4
+ type PluginToolAction<Input> = string | ((input: Input) => string);
5
+ export interface PluginToolExecution {
6
+ readonly operationId: string;
7
+ /** Keep the operation running after the MCP handler returns. The owner must finish it later. */
8
+ deferCompletion(): void;
9
+ }
10
+ export interface PluginToolConfig<InputSchema extends StandardSchemaWithJSON, OutputSchema extends StandardSchemaWithJSON> {
11
+ title: string;
12
+ description: string;
13
+ inputSchema: InputSchema;
14
+ outputSchema: OutputSchema;
15
+ annotations: ToolAnnotations;
16
+ action: PluginToolAction<PluginToolInput<InputSchema>>;
17
+ }
18
+ /** Framework-owned MCP registration boundary that guarantees every plugin tool is audited. */
19
+ export declare class PluginMcpRegistrar {
20
+ private readonly server;
21
+ private readonly operations;
22
+ private readonly pluginId;
23
+ constructor(server: McpServer, operations: OperationLog, pluginId: string);
24
+ registerTool<InputSchema extends StandardSchemaWithJSON, OutputSchema extends StandardSchemaWithJSON>(name: string, config: PluginToolConfig<InputSchema, OutputSchema>, handler: (input: PluginToolInput<InputSchema>, execution: PluginToolExecution) => Promise<unknown> | unknown): void;
25
+ }
26
+ export {};
@@ -0,0 +1,63 @@
1
+ import { asChatRoomError } from "../../core/errors/chatroom-error.js";
2
+ import { mcpTool } from "./tool-support.js";
3
+ /** Framework-owned MCP registration boundary that guarantees every plugin tool is audited. */
4
+ export class PluginMcpRegistrar {
5
+ server;
6
+ operations;
7
+ pluginId;
8
+ constructor(server, operations, pluginId) {
9
+ this.server = server;
10
+ this.operations = operations;
11
+ this.pluginId = pluginId;
12
+ }
13
+ registerTool(name, config, handler) {
14
+ const { action, ...toolConfig } = config;
15
+ const callback = mcpTool(async (input) => {
16
+ const operation = this.operations.start({
17
+ pluginId: this.pluginId,
18
+ source: "mcp",
19
+ action: typeof action === "function" ? action(input) : action,
20
+ input,
21
+ ...operationReferences(input),
22
+ });
23
+ let deferred = false;
24
+ const execution = {
25
+ operationId: operation.operationId,
26
+ deferCompletion() {
27
+ deferred = true;
28
+ },
29
+ };
30
+ try {
31
+ const result = await handler(input, execution);
32
+ if (!deferred)
33
+ this.operations.finish(operation.operationId, "success", result);
34
+ return result;
35
+ }
36
+ catch (error) {
37
+ if (this.operations.get(operation.operationId)?.status === "running") {
38
+ const normalized = asChatRoomError(error);
39
+ this.operations.finish(operation.operationId, "error", null, {
40
+ code: normalized.code,
41
+ message: normalized.message,
42
+ details: normalized.details,
43
+ });
44
+ }
45
+ throw error;
46
+ }
47
+ });
48
+ this.server.registerTool(name, toolConfig, callback);
49
+ }
50
+ }
51
+ function operationReferences(input) {
52
+ if (!input || typeof input !== "object" || Array.isArray(input))
53
+ return {};
54
+ const record = input;
55
+ return {
56
+ ...(typeof record.workspaceId === "string"
57
+ ? { workspaceId: record.workspaceId }
58
+ : {}),
59
+ ...(typeof record.processId === "string"
60
+ ? { processId: record.processId }
61
+ : {}),
62
+ };
63
+ }
@@ -35,8 +35,8 @@ export declare const workspaceOutputSchema: z.ZodObject<{
35
35
  root: z.ZodString;
36
36
  sourceRoot: z.ZodString;
37
37
  mode: z.ZodEnum<{
38
- worktree: "worktree";
39
38
  checkout: "checkout";
39
+ worktree: "worktree";
40
40
  }>;
41
41
  createdAt: z.ZodString;
42
42
  lastUsedAt: z.ZodString;
@@ -14,6 +14,10 @@ export declare class OperationLog {
14
14
  run<T>(request: OperationStart, action: () => Promise<T>): Promise<T>;
15
15
  list(query?: OperationQuery): Operation[];
16
16
  get(operationId: string): Operation | null;
17
+ associate(operationId: string, references: {
18
+ workspaceId?: string | null;
19
+ processId?: string | null;
20
+ }): Operation;
17
21
  clearHistory(): {
18
22
  deleted: number;
19
23
  preserved: number;
@@ -86,6 +86,16 @@ export class OperationLog {
86
86
  get(operationId) {
87
87
  return this.repository.get(operationId);
88
88
  }
89
+ associate(operationId, references) {
90
+ const operation = this.require(operationId);
91
+ if (references.workspaceId !== undefined)
92
+ operation.workspaceId = references.workspaceId;
93
+ if (references.processId !== undefined)
94
+ operation.processId = references.processId;
95
+ this.repository.update(operation);
96
+ this.eventBus.emit({ type: "operation", operation });
97
+ return operation;
98
+ }
89
99
  clearHistory() {
90
100
  const result = this.repository.clearHistory();
91
101
  this.eventBus.emit({ type: "operations-cleared", ...result });
@@ -3,6 +3,10 @@ import { type CloudEntitlement, type CloudLeaseState, type CloudServiceId } from
3
3
  declare const recoveryCredentialSchema: z.ZodObject<{
4
4
  recoveryKey: z.ZodString;
5
5
  }, z.core.$strict>;
6
+ declare const registrationSchema: z.ZodObject<{
7
+ installationId: z.ZodString;
8
+ registered: z.ZodLiteral<true>;
9
+ }, z.core.$strict>;
6
10
  declare const sessionSchema: z.ZodObject<{
7
11
  purchaseToken: z.ZodString;
8
12
  managementUrl: z.ZodString;
@@ -33,6 +37,7 @@ declare const prefixSchema: z.ZodObject<{
33
37
  export declare class CloudApiClient {
34
38
  private readonly baseUrl;
35
39
  constructor(baseUrl: string);
40
+ registerDevice(identity: DeviceIdentity): Promise<z.infer<typeof registrationSchema>>;
36
41
  createManagementSession(identity: DeviceIdentity): Promise<z.infer<typeof sessionSchema>>;
37
42
  status(identity: DeviceIdentity, purchaseToken: string | null): Promise<{
38
43
  managementSessionActive: boolean;
@@ -7,6 +7,12 @@ const recoveryCredentialSchema = z
7
7
  recoveryKey: z.string().regex(/^crr\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/),
8
8
  })
9
9
  .strict();
10
+ const registrationSchema = z
11
+ .object({
12
+ installationId: z.string().uuid(),
13
+ registered: z.literal(true),
14
+ })
15
+ .strict();
10
16
  const sessionSchema = z
11
17
  .object({
12
18
  purchaseToken: z.string().min(24),
@@ -42,6 +48,10 @@ export class CloudApiClient {
42
48
  constructor(baseUrl) {
43
49
  this.baseUrl = baseUrl;
44
50
  }
51
+ async registerDevice(identity) {
52
+ const payload = { devicePublicKey: identity.devicePublicKey };
53
+ return registrationSchema.parse(await this.devicePost("/v1/device/register", "register", identity, payload));
54
+ }
45
55
  async createManagementSession(identity) {
46
56
  const payload = { devicePublicKey: identity.devicePublicKey };
47
57
  return sessionSchema.parse(await this.devicePost("/v1/device/session", "session", identity, payload));
@@ -24,6 +24,7 @@ export declare class CloudController {
24
24
  status(): CloudStatus;
25
25
  start(): Promise<void>;
26
26
  stop(): Promise<void>;
27
+ registerDevice(): Promise<CloudStatus>;
27
28
  managementUrl(): Promise<{
28
29
  url: string;
29
30
  expiresAt: string;
@@ -65,6 +65,11 @@ export class CloudController {
65
65
  if (this.state.lease)
66
66
  this.connection = "disconnected";
67
67
  }
68
+ async registerDevice() {
69
+ await this.api.registerDevice(this.identity());
70
+ this.lastError = null;
71
+ return this.status();
72
+ }
68
73
  async managementUrl() {
69
74
  const current = this.state.managementSession;
70
75
  if (current && Date.parse(current.expiresAt) > Date.now()) {
@@ -1,3 +1,4 @@
1
+ import { PluginMcpRegistrar } from "../mcp/server/plugin-mcp-registrar.js";
1
2
  export class PluginManager {
2
3
  context;
3
4
  plugins;
@@ -13,8 +14,11 @@ export class PluginManager {
13
14
  }
14
15
  }
15
16
  registerMcp(server) {
16
- for (const plugin of this.active)
17
- plugin.registerMcp?.(server);
17
+ for (const plugin of this.active) {
18
+ if (!plugin.registerMcp)
19
+ continue;
20
+ plugin.registerMcp(new PluginMcpRegistrar(server, this.context.operations, plugin.id));
21
+ }
18
22
  }
19
23
  async stop() {
20
24
  for (const plugin of [...this.active].reverse())
@@ -1,4 +1,3 @@
1
- import type { McpServer } from "@modelcontextprotocol/server";
2
- import type { OperationLog } from "../../operations/operation-log.js";
1
+ import type { PluginMcpRegistrar } from "../../mcp/server/plugin-mcp-registrar.js";
3
2
  import type { ProcessSupervisor } from "./process-supervisor.js";
4
- export declare function registerProcessTools(server: McpServer, processes: ProcessSupervisor, operations: OperationLog): void;
3
+ export declare function registerProcessTools(mcp: PluginMcpRegistrar, processes: ProcessSupervisor): void;
@@ -1,9 +1,9 @@
1
1
  import { z } from "zod";
2
- import { closedRead, destructiveLocalMutation, mcpTool, openWorldMutation, processSnapshotSchema, } from "../../mcp/server/tool-support.js";
3
- export function registerProcessTools(server, processes, operations) {
4
- server.registerTool("process_start", {
2
+ import { closedRead, destructiveLocalMutation, openWorldMutation, processSnapshotSchema, } from "../../mcp/server/tool-support.js";
3
+ export function registerProcessTools(mcp, processes) {
4
+ mcp.registerTool("process_start", {
5
5
  title: "Start process",
6
- description: "Start a supervised pipe or PTY process. Returns a ProcessId immediately.",
6
+ description: "Start a supervised executable directly and return a ProcessId immediately. Prefer invoking the target executable without a shell. For compound shell commands, use bash -c rather than bash -lc unless login-shell semantics are explicitly required, because a login shell may replace the inherited PATH (for example, removing NVM-managed Node.js commands).",
7
7
  inputSchema: z.object({
8
8
  command: z.string().min(1),
9
9
  args: z.array(z.string()).default([]),
@@ -14,43 +14,37 @@ export function registerProcessTools(server, processes, operations) {
14
14
  }),
15
15
  outputSchema: processSnapshotSchema,
16
16
  annotations: openWorldMutation,
17
- }, mcpTool((input) => processes.start({
18
- cwd: input.cwd,
19
- command: input.command,
20
- args: input.args,
21
- ...(input.env ? { env: input.env } : {}),
22
- pty: input.pty,
23
- ...(input.timeoutMs === undefined
24
- ? {}
25
- : { timeoutMs: input.timeoutMs }),
26
- }, { source: "mcp" })));
27
- server.registerTool("process_read", {
17
+ action: "start",
18
+ }, (input, execution) => {
19
+ execution.deferCompletion();
20
+ return processes.start({
21
+ cwd: input.cwd,
22
+ command: input.command,
23
+ args: input.args,
24
+ ...(input.env ? { env: input.env } : {}),
25
+ pty: input.pty,
26
+ ...(input.timeoutMs === undefined
27
+ ? {}
28
+ : { timeoutMs: input.timeoutMs }),
29
+ }, { source: "mcp", operationId: execution.operationId });
30
+ });
31
+ mcp.registerTool("process_read", {
28
32
  title: "Read process",
29
33
  description: "Read bounded stdout/stderr and current process state.",
30
34
  inputSchema: z.object({ processId: z.string() }),
31
35
  outputSchema: processSnapshotSchema,
32
36
  annotations: closedRead,
33
- }, mcpTool((input) => operations.run({
34
- pluginId: "process",
35
- source: "mcp",
36
37
  action: "read",
37
- processId: input.processId,
38
- input,
39
- }, () => Promise.resolve(processes.read(input.processId)))));
40
- server.registerTool("process_write", {
38
+ }, (input) => processes.read(input.processId));
39
+ mcp.registerTool("process_write", {
41
40
  title: "Write process stdin",
42
41
  description: "Write to stdin of a running process.",
43
42
  inputSchema: z.object({ processId: z.string(), data: z.string() }),
44
43
  outputSchema: processSnapshotSchema,
45
44
  annotations: openWorldMutation,
46
- }, mcpTool((input) => operations.run({
47
- pluginId: "process",
48
- source: "mcp",
49
45
  action: "write",
50
- processId: input.processId,
51
- input,
52
- }, () => Promise.resolve(processes.write(input.processId, input.data)))));
53
- server.registerTool("process_kill", {
46
+ }, (input) => processes.write(input.processId, input.data));
47
+ mcp.registerTool("process_kill", {
54
48
  title: "Stop process",
55
49
  description: "Terminate or force-kill a supervised process.",
56
50
  inputSchema: z.object({
@@ -59,11 +53,6 @@ export function registerProcessTools(server, processes, operations) {
59
53
  }),
60
54
  outputSchema: processSnapshotSchema,
61
55
  annotations: destructiveLocalMutation,
62
- }, mcpTool((input) => operations.run({
63
- pluginId: "process",
64
- source: "mcp",
65
- action: input.force ? "kill" : "terminate",
66
- processId: input.processId,
67
- input,
68
- }, () => Promise.resolve(processes.kill(input.processId, input.force)))));
56
+ action: (input) => (input.force ? "kill" : "terminate"),
57
+ }, (input) => processes.kill(input.processId, input.force));
69
58
  }
@@ -6,23 +6,20 @@ import { ProcessSupervisor } from "./process-supervisor.js";
6
6
  export const ProcessService = createServiceToken("process");
7
7
  export function createProcessPlugin() {
8
8
  let service = null;
9
- let operations = null;
10
9
  return {
11
10
  id: "process",
12
11
  activate(context) {
13
12
  service = new ProcessSupervisor({ pipe: new PipeProcessBackend(), pty: new PtyProcessBackend() }, context.operations, context.events, context.config.process.maxOutputBytes, context.config.process.defaultTimeoutMs, context.config.process.maxCompletedProcesses);
14
- operations = context.operations;
15
13
  context.services.provide(ProcessService, service);
16
14
  },
17
- registerMcp(server) {
18
- if (!service || !operations)
15
+ registerMcp(mcp) {
16
+ if (!service)
19
17
  throw new Error("Process plugin is not active");
20
- registerProcessTools(server, service, operations);
18
+ registerProcessTools(mcp, service);
21
19
  },
22
20
  async deactivate() {
23
21
  await service?.shutdown();
24
22
  service = null;
25
- operations = null;
26
23
  },
27
24
  };
28
25
  }
@@ -6,6 +6,7 @@ import type { ProcessBackend } from "./backend.js";
6
6
  interface ProcessOperationContext {
7
7
  source: OperationSource;
8
8
  action?: string;
9
+ operationId?: string;
9
10
  }
10
11
  export declare class ProcessSupervisor {
11
12
  private readonly backends;
@@ -28,20 +28,24 @@ export class ProcessSupervisor {
28
28
  throw new ChatRoomError("CONFLICT", "Process supervisor is shutting down");
29
29
  const processId = `proc_${randomUUID()}`;
30
30
  const args = request.args ?? [];
31
- const operation = this.operations.start({
32
- pluginId: "process",
33
- source: operationContext.source,
34
- action: operationContext.action ?? "start",
35
- processId,
36
- input: {
37
- command: request.command,
38
- args,
39
- cwd: request.cwd,
40
- pty: request.pty ?? false,
41
- timeoutMs: request.timeoutMs ?? this.defaultTimeoutMs,
42
- env: request.env ?? {},
43
- },
44
- });
31
+ const adoptedOperation = operationContext.operationId !== undefined;
32
+ const operationId = operationContext.operationId ??
33
+ this.operations.start({
34
+ pluginId: "process",
35
+ source: operationContext.source,
36
+ action: operationContext.action ?? "start",
37
+ processId,
38
+ input: {
39
+ command: request.command,
40
+ args,
41
+ cwd: request.cwd,
42
+ pty: request.pty ?? false,
43
+ timeoutMs: request.timeoutMs ?? this.defaultTimeoutMs,
44
+ env: request.env ?? {},
45
+ },
46
+ }).operationId;
47
+ if (adoptedOperation)
48
+ this.operations.associate(operationId, { processId });
45
49
  let backend;
46
50
  try {
47
51
  // Child environments inherit only the runtime allowlist, then apply explicit request overrides.
@@ -51,7 +55,8 @@ export class ProcessSupervisor {
51
55
  });
52
56
  }
53
57
  catch (error) {
54
- this.operations.finish(operation.operationId, "error", null, error);
58
+ if (!adoptedOperation)
59
+ this.operations.finish(operationId, "error", null, error);
55
60
  throw new ChatRoomError("PROCESS_FAILED", `Failed to start process: ${request.command}`, undefined, { cause: error });
56
61
  }
57
62
  let resolveSettled;
@@ -71,7 +76,7 @@ export class ProcessSupervisor {
71
76
  timeout: null,
72
77
  forceTimeout: null,
73
78
  timedOut: false,
74
- operationId: operation.operationId,
79
+ operationId,
75
80
  settled,
76
81
  resolveSettled,
77
82
  };
@@ -1,6 +1,6 @@
1
- import type { McpServer } from "@modelcontextprotocol/server";
2
1
  import type { ChatRoomConfig } from "../config/types.js";
3
2
  import type { AppDatabase } from "../infrastructure/database/app-database.js";
3
+ import type { PluginMcpRegistrar } from "../mcp/server/plugin-mcp-registrar.js";
4
4
  import type { OperationLog } from "../operations/operation-log.js";
5
5
  import type { RuntimeEventBus } from "../app/event-bus.js";
6
6
  import type { ExternalAccessRegistry } from "../app/external-access-registry.js";
@@ -27,6 +27,6 @@ export interface PluginContext {
27
27
  export interface InternalPlugin {
28
28
  id: string;
29
29
  activate(context: PluginContext): Promise<void> | void;
30
- registerMcp?(server: McpServer): void;
30
+ registerMcp?(mcp: PluginMcpRegistrar): void;
31
31
  deactivate?(): Promise<void> | void;
32
32
  }
@@ -5,6 +5,7 @@ import { asyncRoute } from "../../../presentation/http/http-utils.js";
5
5
  export function createCloudApiRouter(controller, operations) {
6
6
  const router = Router();
7
7
  router.get("/cloud/status", (_req, res) => res.json(controller.status()));
8
+ router.post("/cloud/register", asyncRoute(async (_req, res) => res.json(await operations.run({ pluginId: "cloud", source: "gui", action: "register" }, () => controller.registerDevice()))));
8
9
  router.post("/cloud/sync", asyncRoute(async (_req, res) => res.json(await operations.run({ pluginId: "cloud", source: "gui", action: "sync" }, () => controller.syncStatus()))));
9
10
  router.post("/cloud/management", asyncRoute(async (_req, res) => res.json(await operations.run({ pluginId: "cloud", source: "gui", action: "management" }, () => controller.managementUrl()))));
10
11
  router.post("/cloud/restore", asyncRoute(async (req, res) => {
@@ -1,4 +1,3 @@
1
- import type { McpServer } from "@modelcontextprotocol/server";
2
- import type { OperationLog } from "../../operations/operation-log.js";
1
+ import type { PluginMcpRegistrar } from "../../mcp/server/plugin-mcp-registrar.js";
3
2
  import type { WorkspaceService } from "./workspace-service.js";
4
- export declare function registerWorkspaceTools(server: McpServer, workspaces: WorkspaceService, operations: OperationLog): void;
3
+ export declare function registerWorkspaceTools(mcp: PluginMcpRegistrar, workspaces: WorkspaceService): void;
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
- import { changeSetSchema, closedRead, destructiveLocalMutation, fileInfoSchema, fileReadOutputSchema, fileWriteOutputSchema, localMutation, localWrite, mcpTool, searchMatchSchema, workspaceOutputSchema, } from "../../mcp/server/tool-support.js";
3
- export function registerWorkspaceTools(server, workspaces, operations) {
4
- server.registerTool("open_workspace", {
2
+ import { changeSetSchema, closedRead, destructiveLocalMutation, fileInfoSchema, fileReadOutputSchema, fileWriteOutputSchema, localMutation, localWrite, searchMatchSchema, workspaceOutputSchema, } from "../../mcp/server/tool-support.js";
3
+ export function registerWorkspaceTools(mcp, workspaces) {
4
+ mcp.registerTool("open_workspace", {
5
5
  title: "Open workspace",
6
6
  description: "Open or reuse an approved checkout, or create a new isolated managed Git worktree from the source checkout HEAD.",
7
7
  inputSchema: z.object({
@@ -10,11 +10,12 @@ export function registerWorkspaceTools(server, workspaces, operations) {
10
10
  }),
11
11
  outputSchema: workspaceOutputSchema,
12
12
  annotations: localMutation,
13
- }, mcpTool((input) => operations.run({ pluginId: "workspace", source: "mcp", action: "open", input }, () => workspaces.open({
13
+ action: "open",
14
+ }, (input) => workspaces.open({
14
15
  path: input.path,
15
16
  ...(input.mode ? { mode: input.mode } : {}),
16
- }))));
17
- server.registerTool("remove_workspace", {
17
+ }));
18
+ mcp.registerTool("remove_workspace", {
18
19
  title: "Remove workspace",
19
20
  description: "Unregister a checkout workspace or remove a managed worktree. Dirty worktrees require force=true.",
20
21
  inputSchema: z.object({
@@ -27,17 +28,12 @@ export function registerWorkspaceTools(server, workspaces, operations) {
27
28
  mode: z.enum(["checkout", "worktree"]),
28
29
  }),
29
30
  annotations: destructiveLocalMutation,
30
- }, mcpTool(async (input) => {
31
- const removed = await operations.run({
32
- pluginId: "workspace",
33
- source: "mcp",
34
- action: "remove",
35
- workspaceId: input.workspaceId,
36
- input,
37
- }, () => workspaces.remove(input.workspaceId, input.force ?? false));
31
+ action: "remove",
32
+ }, async (input) => {
33
+ const removed = await workspaces.remove(input.workspaceId, input.force ?? false);
38
34
  return { removed: true, workspaceId: removed.id, mode: removed.mode };
39
- }));
40
- server.registerTool("fs_read", {
35
+ });
36
+ mcp.registerTool("fs_read", {
41
37
  title: "Read file",
42
38
  description: "Read a workspace-relative file through the ChatRoom filesystem boundary.",
43
39
  inputSchema: z.object({
@@ -52,17 +48,12 @@ export function registerWorkspaceTools(server, workspaces, operations) {
52
48
  }),
53
49
  outputSchema: fileReadOutputSchema,
54
50
  annotations: closedRead,
55
- }, mcpTool(async (input) => {
51
+ action: "fs.read",
52
+ }, async (input) => {
56
53
  const fs = await workspaces.fs(input.workspaceId);
57
- return operations.run({
58
- pluginId: "workspace",
59
- source: "mcp",
60
- action: "fs.read",
61
- workspaceId: input.workspaceId,
62
- input,
63
- }, () => fs.read(input.path, input.maxBytes === undefined ? {} : { maxBytes: input.maxBytes }));
64
- }));
65
- server.registerTool("fs_write", {
54
+ return fs.read(input.path, input.maxBytes === undefined ? {} : { maxBytes: input.maxBytes });
55
+ });
56
+ mcp.registerTool("fs_write", {
66
57
  title: "Write file",
67
58
  description: "Atomically write a workspace-relative file.",
68
59
  inputSchema: z.object({
@@ -72,17 +63,12 @@ export function registerWorkspaceTools(server, workspaces, operations) {
72
63
  }),
73
64
  outputSchema: fileWriteOutputSchema,
74
65
  annotations: localWrite,
75
- }, mcpTool(async (input) => {
66
+ action: "fs.write",
67
+ }, async (input) => {
76
68
  const fs = await workspaces.fs(input.workspaceId);
77
- return operations.run({
78
- pluginId: "workspace",
79
- source: "mcp",
80
- action: "fs.write",
81
- workspaceId: input.workspaceId,
82
- input,
83
- }, () => fs.write(input.path, input.content));
84
- }));
85
- server.registerTool("fs_list", {
69
+ return fs.write(input.path, input.content);
70
+ });
71
+ mcp.registerTool("fs_list", {
86
72
  title: "List files",
87
73
  description: "List files under a workspace-relative directory.",
88
74
  inputSchema: z.object({
@@ -92,19 +78,14 @@ export function registerWorkspaceTools(server, workspaces, operations) {
92
78
  }),
93
79
  outputSchema: z.object({ files: z.array(fileInfoSchema) }),
94
80
  annotations: closedRead,
95
- }, mcpTool(async (input) => {
81
+ action: "fs.list",
82
+ }, async (input) => {
96
83
  const fs = await workspaces.fs(input.workspaceId);
97
84
  return {
98
- files: await operations.run({
99
- pluginId: "workspace",
100
- source: "mcp",
101
- action: "fs.list",
102
- workspaceId: input.workspaceId,
103
- input,
104
- }, () => fs.list(input.path, { recursive: input.recursive })),
85
+ files: await fs.list(input.path, { recursive: input.recursive }),
105
86
  };
106
- }));
107
- server.registerTool("fs_search", {
87
+ });
88
+ mcp.registerTool("fs_search", {
108
89
  title: "Search files",
109
90
  description: "Search text under a workspace.",
110
91
  inputSchema: z.object({
@@ -115,22 +96,17 @@ export function registerWorkspaceTools(server, workspaces, operations) {
115
96
  }),
116
97
  outputSchema: z.object({ matches: z.array(searchMatchSchema) }),
117
98
  annotations: closedRead,
118
- }, mcpTool(async (input) => {
99
+ action: "fs.search",
100
+ }, async (input) => {
119
101
  const fs = await workspaces.fs(input.workspaceId);
120
102
  return {
121
- matches: await operations.run({
122
- pluginId: "workspace",
123
- source: "mcp",
124
- action: "fs.search",
125
- workspaceId: input.workspaceId,
126
- input,
127
- }, () => fs.search(input.query, {
103
+ matches: await fs.search(input.query, {
128
104
  path: input.path,
129
105
  maxResults: input.maxResults,
130
- })),
106
+ }),
131
107
  };
132
- }));
133
- server.registerTool("fs_patch", {
108
+ });
109
+ mcp.registerTool("fs_patch", {
134
110
  title: "Transactional patch",
135
111
  description: "Validate all replacements before committing a multi-file text patch.",
136
112
  inputSchema: z.object({
@@ -148,21 +124,16 @@ export function registerWorkspaceTools(server, workspaces, operations) {
148
124
  }),
149
125
  outputSchema: changeSetSchema,
150
126
  annotations: destructiveLocalMutation,
151
- }, mcpTool(async (input) => {
127
+ action: "fs.patch",
128
+ }, async (input) => {
152
129
  const fs = await workspaces.fs(input.workspaceId);
153
- return operations.run({
154
- pluginId: "workspace",
155
- source: "mcp",
156
- action: "fs.patch",
157
- workspaceId: input.workspaceId,
158
- input,
159
- }, () => fs.patch(input.replacements.map((item) => ({
130
+ return fs.patch(input.replacements.map((item) => ({
160
131
  path: item.path,
161
132
  oldText: item.oldText,
162
133
  newText: item.newText,
163
134
  ...(item.occurrence === undefined
164
135
  ? {}
165
136
  : { occurrence: item.occurrence }),
166
- }))));
167
- }));
137
+ })));
138
+ });
168
139
  }