pi-roundtable-mcp 0.1.0 → 0.3.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.3.0] - 2026-10-01
9
+
10
+ ### Added
11
+
12
+ - `Connectors.resolve(serverName)` reads a virtual server by name, and `Connectors.admin` (`gateways()`, `servers()`) shows what ContextForge holds, for a host that manages some servers itself or shows their state.
13
+ - The `REMOTE_MCP` service of `remoteMcp`: the owner's bundles and grants, read only, and `describeGrant(client, grant)`, so a host can show them in its own status view.
14
+ - The types `GatewayState`, `ChannelBundle`, `ChannelGrant`, and `RemoteMcpService`.
15
+
16
+ ## [0.2.0] - 2026-10-01
17
+
18
+ ### Added
19
+
20
+ - `remoteMcp({ toolNames })`: the names of the two tools at `/mcp/personal`, for agents that are already set up with other names. The default descriptions name each other with the chosen names. A name that is not letters, digits, `_` or `-` (up to 64), or two equal names, is refused when the plugin is created.
21
+
22
+ ### Changed
23
+
24
+ - `RemoteMcpMessages.dispatchDescription` and `resultDescription` are now functions that receive the tool names, so a description can refer to the other tool by its real name. A host that set either as a string gives `() => "..."` instead.
25
+
8
26
  ## [0.1.0] - 2026-10-01
9
27
 
10
28
  First release: two plugins for pi-roundtable 0.4, extracted from a private host.
package/README.md CHANGED
@@ -90,6 +90,8 @@ It provides the `CONNECTORS` service (`Connectors`):
90
90
  | `servers()` | The virtual servers (name, URL, tool names) of the connectors that could be read |
91
91
  | `profileSources()` | One `{ name, description, serverName }` per connector, the shape a host builds a per-agent MCP profile from |
92
92
  | `token` | The bearer token that the virtual servers' URLs expect; one per process, valid for a year |
93
+ | `resolve(serverName)` | Reads one virtual server's URL and tools by name, for servers the host manages itself rather than as connectors |
94
+ | `admin` | `gateways()` and `servers()`: what ContextForge holds, upstream gateways with their state and every virtual server with its tool names, for a host's status view |
93
95
 
94
96
  The plugin does not decide which agent uses which connector.
95
97
  The host reads the service, builds its own profiles, and compares `version` with the one it built at.
@@ -153,7 +155,8 @@ The owner manages connectors on Discord with `/<root> connector`, where `<root>`
153
155
  | `publicUrl` | `string` | required | The HTTPS address that reaches the host's `public` listener. Granted-channel URLs are built on its origin |
154
156
  | `persona` | `string` | a short neutral prompt | The system prompt of the default `remote` conversations |
155
157
  | `answer`, `claim` | see [below](#when-the-host-runs-the-conversations-itself) | the core's runtime | Give both, or neither |
156
- | `messages` | `Partial<RemoteMcpMessages>` | English | The relay note, the tool descriptions, and the Discord text, in your wording |
158
+ | `messages` | `Partial<RemoteMcpMessages>` | English | The relay note, the tool descriptions, and the Discord text, in your wording. `dispatchDescription` and `resultDescription` are functions that receive the tool names |
159
+ | `toolNames` | `{ dispatch?: string; result?: string }` | `agent_dispatch`, `agent_result` | The names of the two tools at `/mcp/personal`, for agents that are already set up with other names. The default descriptions follow them |
157
160
 
158
161
  The plugin serves two endpoints on the host's `public` listener:
159
162
 
@@ -176,6 +179,13 @@ The owner grants channels on Discord with `/<root> mcp`:
176
179
  | `describe` | Changes the channel name and purpose that the agent sees (Discord's own name and topic stay) |
177
180
  | `token` | Replaces a bundle's MCP URL; the old one stops working at once and the channel settings stay |
178
181
 
182
+ The plugin provides the `REMOTE_MCP` service (`RemoteMcpService`) for a host that shows the owner what outside agents may reach:
183
+
184
+ | Member | What it is |
185
+ | --- | --- |
186
+ | `grants` | `bundles()` and `grants(bundleId?)`, read only: every bundle, and the channel grants of one bundle or of all |
187
+ | `describeGrant(client, grant)` | One grant as lines of text for a Discord message: the name the agent sees, where the channel is, its purpose, and the allowed operations, in the plugin's wording |
188
+
179
189
  #### The default conversation
180
190
 
181
191
  Without `answer` and `claim`, a relayed turn runs on the core: `context.turns.run` of kind `remote`, for an owner-tier speaker, in the channel `mcp:<session>`, through the agent server's runtime.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-roundtable-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP connectors through ContextForge and a remote MCP endpoint for pi-roundtable",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,6 +54,10 @@ export interface Connectors {
54
54
  profileSources(): ConnectorProfileSource[];
55
55
  /** The bearer token that ContextForge's virtual server URLs expect; one per process. */
56
56
  readonly token: string;
57
+ /** Reads a virtual server's endpoint and tools by name, for the servers a host manages itself. */
58
+ resolve(serverName: string): Promise<VirtualServer>;
59
+ /** What ContextForge holds, upstream gateways and virtual servers, for a host's status view. */
60
+ readonly admin: Pick<ContextForgeAdmin, "gateways" | "servers">;
57
61
  }
58
62
 
59
63
  export const CONNECTORS = serviceKey<Connectors>(
@@ -89,9 +93,11 @@ export function mcpConnectors(options: McpConnectorsOptions): RoundtablePlugin {
89
93
  setup: async ({ database, services, logger }) => {
90
94
  const { jwtSecret, user } = options.contextForge;
91
95
  const token = contextForgeToken(jwtSecret, user, TOKEN_TTL_SECONDS);
96
+ const admin = new ContextForgeAdmin(url, token);
97
+ const resolve = (name: string) => resolveVirtualServer(url, token, name);
92
98
  const registry = await ConnectorRegistry.attach(database(), {
93
- admin: new ContextForgeAdmin(url, token),
94
- resolve: (name) => resolveVirtualServer(url, token, name),
99
+ admin,
100
+ resolve,
95
101
  logger,
96
102
  serverPrefix: options.serverPrefix ?? DEFAULT_SERVER_PREFIX,
97
103
  maxToolName: options.maxToolName ?? DEFAULT_MAX_TOOL_NAME,
@@ -107,6 +113,11 @@ export function mcpConnectors(options: McpConnectorsOptions): RoundtablePlugin {
107
113
  servers: () => registry.servers(),
108
114
  profileSources: () => registry.profileSources(),
109
115
  token,
116
+ resolve,
117
+ admin: {
118
+ gateways: () => admin.gateways(),
119
+ servers: () => admin.servers(),
120
+ },
110
121
  });
111
122
  return {};
112
123
  },
package/src/index.ts CHANGED
@@ -11,9 +11,16 @@ export type {
11
11
  McpConnectorsOptions,
12
12
  } from "./connectors/connectors-plugin.ts";
13
13
  export { CONNECTORS, mcpConnectors } from "./connectors/connectors-plugin.ts";
14
- export type { UpstreamAuth } from "./connectors/contextforge.ts";
14
+ export type { GatewayState, UpstreamAuth } from "./connectors/contextforge.ts";
15
15
  export type { ConnectorMessages } from "./connectors/messages.ts";
16
- export type { RemoteMcpMessages } from "./remote-mcp/messages.ts";
16
+ export type {
17
+ ChannelBundle,
18
+ ChannelGrant,
19
+ } from "./remote-mcp/channel-grants.ts";
20
+ export type {
21
+ RemoteMcpMessages,
22
+ RemoteToolNames,
23
+ } from "./remote-mcp/messages.ts";
17
24
  export type { RemoteClaimHooks } from "./remote-mcp/remote-claim.ts";
18
25
  export type {
19
26
  DefaultConversationOptions,
@@ -21,3 +28,5 @@ export type {
21
28
  RemoteMcpOptions,
22
29
  } from "./remote-mcp/remote-mcp-plugin.ts";
23
30
  export { remoteMcp } from "./remote-mcp/remote-mcp-plugin.ts";
31
+ export type { RemoteMcpService } from "./remote-mcp/remote-mcp-service.ts";
32
+ export { REMOTE_MCP } from "./remote-mcp/remote-mcp-service.ts";
@@ -23,14 +23,17 @@ import {
23
23
  hashChannelToken,
24
24
  } from "./channel-grants.ts";
25
25
  import { runGrantedTool } from "./channel-tools.ts";
26
- import { REMOTE_MCP_MESSAGES, type RemoteMcpMessages } from "./messages.ts";
26
+ import {
27
+ DEFAULT_TOOL_NAMES,
28
+ REMOTE_MCP_MESSAGES,
29
+ type RemoteMcpMessages,
30
+ type RemoteToolNames,
31
+ } from "./messages.ts";
27
32
  import { type RemoteAgent, RemoteAgentError } from "./remote-agent.ts";
28
33
 
29
34
  /** Requests may carry uploads; anything larger is refused before it is parsed. */
30
35
  const MAX_BODY_BYTES = 12 * 1024 * 1024;
31
36
 
32
- export const DISPATCH_TOOL = "agent_dispatch";
33
- export const RESULT_TOOL = "agent_result";
34
37
  const LIST_CHANNELS_TOOL = "discord_list_authorized_channels";
35
38
 
36
39
  export interface McpGatewayOptions {
@@ -42,6 +45,8 @@ export interface McpGatewayOptions {
42
45
  executor(): ChannelExecutor | undefined;
43
46
  logger: Logger;
44
47
  messages?: RemoteMcpMessages;
48
+ /** The names of the dispatch and result tools; `agent_dispatch` and `agent_result` by default. */
49
+ toolNames?: RemoteToolNames;
45
50
  }
46
51
 
47
52
  interface ToolSpec {
@@ -73,11 +78,13 @@ const ResultInput = Type.Object(
73
78
  export class McpGateway {
74
79
  readonly #options: McpGatewayOptions;
75
80
  readonly #text: RemoteMcpMessages;
81
+ readonly #tools: RemoteToolNames;
76
82
  readonly #dispatchInput: TSchema;
77
83
 
78
84
  constructor(options: McpGatewayOptions) {
79
85
  this.#options = options;
80
86
  this.#text = options.messages ?? REMOTE_MCP_MESSAGES;
87
+ this.#tools = options.toolNames ?? DEFAULT_TOOL_NAMES;
81
88
  this.#dispatchInput = Type.Object(
82
89
  {
83
90
  message: Type.String({ minLength: 1 }),
@@ -172,8 +179,8 @@ export class McpGateway {
172
179
  return [
173
180
  {
174
181
  tool: {
175
- name: DISPATCH_TOOL,
176
- description: this.#text.dispatchDescription,
182
+ name: this.#tools.dispatch,
183
+ description: this.#text.dispatchDescription(this.#tools),
177
184
  inputSchema: jsonSchema(this.#dispatchInput),
178
185
  },
179
186
  call: async (args) => {
@@ -187,8 +194,8 @@ export class McpGateway {
187
194
  },
188
195
  {
189
196
  tool: {
190
- name: RESULT_TOOL,
191
- description: this.#text.resultDescription,
197
+ name: this.#tools.result,
198
+ description: this.#text.resultDescription(this.#tools),
192
199
  inputSchema: jsonSchema(ResultInput),
193
200
  annotations: { readOnlyHint: true, idempotentHint: true },
194
201
  },
@@ -1,3 +1,14 @@
1
+ /** The names of the two tools `/mcp/personal` offers. */
2
+ export interface RemoteToolNames {
3
+ dispatch: string;
4
+ result: string;
5
+ }
6
+
7
+ export const DEFAULT_TOOL_NAMES: RemoteToolNames = {
8
+ dispatch: "agent_dispatch",
9
+ result: "agent_result",
10
+ };
11
+
1
12
  /**
2
13
  * The text of the remote MCP plugin: what outside agents read in tool descriptions and errors,
3
14
  * and what the owner reads in the Discord commands.
@@ -10,8 +21,10 @@ export interface RemoteMcpMessages {
10
21
  /** Sent for a granted tool call that failed in a way the agent must not retry. */
11
22
  operationUnfinished: string;
12
23
 
13
- dispatchDescription: string;
14
- resultDescription: string;
24
+ /** The description of the dispatch tool; `tools` holds the names the host chose. */
25
+ dispatchDescription(tools: RemoteToolNames): string;
26
+ /** The description of the result tool. */
27
+ resultDescription(tools: RemoteToolNames): string;
15
28
  sessionIdDescription: string;
16
29
  listChannelsDescription: string;
17
30
  /** Appended to every granted channel tool's description. */
@@ -87,12 +100,12 @@ export const REMOTE_MCP_MESSAGES: RemoteMcpMessages = {
87
100
  operationUnfinished:
88
101
  "The operation did not complete. Check the audit log of the grants on Discord; do not retry automatically.",
89
102
 
90
- dispatchDescription:
103
+ dispatchDescription: (tools) =>
91
104
  "Start or continue a conversation turn with the owner's personal agent, on the owner's behalf. " +
92
- "Returns { runId, sessionId } at once without waiting for the agent to finish; poll agent_result with the runId. " +
105
+ `Returns { runId, sessionId } at once without waiting for the agent to finish; poll ${tools.result} with the runId. ` +
93
106
  "Omit sessionId to start a new conversation; pass a returned sessionId to continue it.",
94
- resultDescription:
95
- "Poll a run started by agent_dispatch. Returns { status: 'working' | 'completed' | 'failed', text?, error? }.",
107
+ resultDescription: (tools) =>
108
+ `Poll a run started by ${tools.dispatch}. Returns { status: 'working' | 'completed' | 'failed', text?, error? }.`,
96
109
  sessionIdDescription:
97
110
  "A sessionId returned earlier; omit it to start a new conversation",
98
111
  listChannelsDescription:
@@ -18,9 +18,15 @@ import {
18
18
  } from "./default-conversation.ts";
19
19
  import { McpGateway } from "./mcp-gateway.ts";
20
20
  import { mcpGrantCommands } from "./mcp-grant-commands.ts";
21
- import { type RemoteMcpMessages, remoteMcpMessages } from "./messages.ts";
21
+ import {
22
+ DEFAULT_TOOL_NAMES,
23
+ type RemoteMcpMessages,
24
+ type RemoteToolNames,
25
+ remoteMcpMessages,
26
+ } from "./messages.ts";
22
27
  import { RemoteAgent } from "./remote-agent.ts";
23
28
  import { type RemoteClaimHooks, remoteClaim } from "./remote-claim.ts";
29
+ import { REMOTE_MCP, remoteMcpService } from "./remote-mcp-service.ts";
24
30
  import { RemoteSessionStore } from "./remote-session-store.ts";
25
31
  import { RemoteSessionSweeper } from "./session-sweeper.ts";
26
32
 
@@ -37,6 +43,11 @@ interface RemoteMcpBaseOptions {
37
43
  publicUrl: string;
38
44
  /** The Discord text, the relay note, and the tool descriptions in your wording; English by default. */
39
45
  messages?: Partial<RemoteMcpMessages>;
46
+ /**
47
+ * The names of the two tools at `/mcp/personal`, for agents already set up with other names;
48
+ * `agent_dispatch` and `agent_result` by default. The default descriptions follow the names.
49
+ */
50
+ toolNames?: Partial<RemoteToolNames>;
40
51
  }
41
52
 
42
53
  /** Remote turns run on the core's runtime: nothing more to give. */
@@ -63,6 +74,21 @@ export interface HostConversationOptions {
63
74
  export type RemoteMcpOptions = RemoteMcpBaseOptions &
64
75
  (DefaultConversationOptions | HostConversationOptions);
65
76
 
77
+ const TOOL_NAME = /^[A-Za-z0-9_-]{1,64}$/;
78
+
79
+ /** The tool names with the host's overrides laid over the defaults; refuses names a client cannot use. */
80
+ function toolNamesOf(options: RemoteMcpOptions): RemoteToolNames {
81
+ const names = { ...DEFAULT_TOOL_NAMES, ...options.toolNames };
82
+ for (const name of Object.values(names))
83
+ if (!TOOL_NAME.test(name))
84
+ throw new ConfigError(
85
+ `remote-mcp: toolNames: "${name}" is not a tool name (letters, digits, _ and -, up to 64)`,
86
+ );
87
+ if (names.dispatch === names.result)
88
+ throw new ConfigError("remote-mcp: toolNames: the two names must differ");
89
+ return names;
90
+ }
91
+
66
92
  function checkOptions(options: RemoteMcpOptions): void {
67
93
  if (!options.dispatchToken)
68
94
  throw new ConfigError("remote-mcp: dispatchToken is empty");
@@ -79,13 +105,16 @@ function checkOptions(options: RemoteMcpOptions): void {
79
105
  * An MCP server over HTTP for outside agents: `/mcp/personal` relays turns to the owner's agent
80
106
  * and returns the result when polled, and `/mcp/discord/<token>` offers the Discord channel
81
107
  * tools granted to one bundle. Grants are approved on Discord with `/<root> mcp`.
108
+ * Provides `REMOTE_MCP`.
82
109
  */
83
110
  export function remoteMcp(options: RemoteMcpOptions): RoundtablePlugin {
84
111
  checkOptions(options);
85
112
  const text = remoteMcpMessages(options.messages);
113
+ const toolNames = toolNamesOf(options);
86
114
  return definePlugin({
87
115
  name: "remote-mcp",
88
116
  requires: [DISCORD],
117
+ provides: [REMOTE_MCP],
89
118
  migrations: [ChannelGrantStore.migration, RemoteSessionStore.migration],
90
119
  setup: async (context) => {
91
120
  const { services, logger, conversations, database } = context;
@@ -105,6 +134,7 @@ export function remoteMcp(options: RemoteMcpOptions): RoundtablePlugin {
105
134
  executor: () => discord.connection.channelExecutor(),
106
135
  logger,
107
136
  messages: text,
137
+ toolNames,
108
138
  });
109
139
  const sweeper = new RemoteSessionSweeper({
110
140
  sessions,
@@ -115,6 +145,7 @@ export function remoteMcp(options: RemoteMcpOptions): RoundtablePlugin {
115
145
  discord.commands.add(
116
146
  mcpGrantCommands(discord.guard, grants, options.publicUrl, text),
117
147
  );
148
+ services.provide(REMOTE_MCP, remoteMcpService(grants, text));
118
149
  return {
119
150
  services: [
120
151
  {
@@ -0,0 +1,31 @@
1
+ import type { Client } from "discord.js";
2
+ import { serviceKey } from "pi-roundtable";
3
+ import type { ChannelGrant, ChannelGrantStore } from "./channel-grants.ts";
4
+ import { describeGrant } from "./mcp-grant-panels.ts";
5
+ import type { RemoteMcpMessages } from "./messages.ts";
6
+
7
+ /** What a host reads to show the owner which channels outside agents may use. */
8
+ export interface RemoteMcpService {
9
+ /** The owner's bundles and their channel grants, read only. */
10
+ grants: Pick<ChannelGrantStore, "bundles" | "grants">;
11
+ /** One grant as lines of text: the agent's name for the channel, where it is, its purpose, and the allowed operations. */
12
+ describeGrant(client: Client, grant: ChannelGrant): Promise<string>;
13
+ }
14
+
15
+ export const REMOTE_MCP = serviceKey<RemoteMcpService>(
16
+ "pi-roundtable-mcp.remote-mcp",
17
+ );
18
+
19
+ /** The service over the plugin's grant store and its wording. */
20
+ export function remoteMcpService(
21
+ grants: ChannelGrantStore,
22
+ text: RemoteMcpMessages,
23
+ ): RemoteMcpService {
24
+ return {
25
+ grants: {
26
+ bundles: () => grants.bundles(),
27
+ grants: (...args) => grants.grants(...args),
28
+ },
29
+ describeGrant: (client, grant) => describeGrant(client, grant, text),
30
+ };
31
+ }