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 +18 -0
- package/README.md +11 -1
- package/package.json +1 -1
- package/src/connectors/connectors-plugin.ts +13 -2
- package/src/index.ts +11 -2
- package/src/remote-mcp/mcp-gateway.ts +14 -7
- package/src/remote-mcp/messages.ts +19 -6
- package/src/remote-mcp/remote-mcp-plugin.ts +32 -1
- package/src/remote-mcp/remote-mcp-service.ts +31 -0
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
|
@@ -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
|
|
94
|
-
resolve
|
|
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 {
|
|
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 {
|
|
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:
|
|
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:
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
+
}
|