pi-roundtable-mcp 0.1.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/LICENSE +21 -0
- package/README.md +273 -0
- package/examples/connector-profiles.ts +36 -0
- package/examples/host-conversation.ts +22 -0
- package/examples/roundtable.config.ts +33 -0
- package/package.json +50 -0
- package/src/connectors/connector-commands.ts +325 -0
- package/src/connectors/connector-registry.ts +340 -0
- package/src/connectors/connectors-plugin.ts +114 -0
- package/src/connectors/contextforge.ts +371 -0
- package/src/connectors/messages.ts +119 -0
- package/src/index.ts +23 -0
- package/src/remote-mcp/channel-grants.ts +308 -0
- package/src/remote-mcp/channel-tools.ts +53 -0
- package/src/remote-mcp/default-conversation.ts +75 -0
- package/src/remote-mcp/mcp-gateway.ts +345 -0
- package/src/remote-mcp/mcp-grant-commands.ts +100 -0
- package/src/remote-mcp/mcp-grant-flow.ts +348 -0
- package/src/remote-mcp/mcp-grant-panels.ts +174 -0
- package/src/remote-mcp/messages.ts +195 -0
- package/src/remote-mcp/remote-agent.ts +127 -0
- package/src/remote-mcp/remote-claim.ts +51 -0
- package/src/remote-mcp/remote-mcp-plugin.ts +148 -0
- package/src/remote-mcp/remote-session-store.ts +55 -0
- package/src/remote-mcp/session-sweeper.ts +78 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-10-01
|
|
9
|
+
|
|
10
|
+
First release: two plugins for pi-roundtable 0.4, extracted from a private host.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `mcpConnectors(options)`: the owner's MCP connectors through ContextForge. `/<root> connector add | list | describe | remove` manage them from Discord, with a private form for the token. It provides the `CONNECTORS` service (`version`, `list()`, `servers()`, `profileSources()`, and the ContextForge `token`), so a host builds its per-agent MCP profiles itself. Options: `contextForge` (`url`, `jwtSecret`, `user`), `serverPrefix` (default `roundtable-conn-`), `maxToolName` (default 45), and `messages`.
|
|
15
|
+
- `remoteMcp(options)`: an MCP server over HTTP on the host's `public` listener for agents outside Discord. `/mcp/personal` relays turns to the owner's agent (`agent_dispatch`, `agent_result`); `/mcp/discord/<token>` offers the Discord channel tools granted to one bundle. `/<root> mcp authorize | grants | revoke | describe | token` grant channels on Discord. Idle remote sessions are deleted after 14 days.
|
|
16
|
+
- Without `answer` and `claim`, `remoteMcp` runs a relayed turn on the core: `context.turns.run` of kind `remote` for an owner-tier speaker, with a `remote` persona (`persona` option, a short neutral prompt by default). A host that runs the owner's conversations itself gives `answer` and `claim` together.
|
|
17
|
+
- A `messages` option on both plugins; the English text is the default. pi-roundtable's message catalog is closed to plugins, so these plugins carry their own.
|
|
18
|
+
- The types `Connector`, `ConnectorProfileSource`, `Connectors`, `ConnectorMessages`, `RemoteMcpMessages`, `RemoteClaimHooks`, and the option types.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wayne930242
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# pi-roundtable-mcp
|
|
2
|
+
|
|
3
|
+
Two plugins for [pi-roundtable](https://github.com/wayne930242/pi-roundtable), the Discord agent server:
|
|
4
|
+
|
|
5
|
+
- `mcpConnectors`: the owner's own MCP connectors.
|
|
6
|
+
The owner adds an external MCP server (Notion, a calendar, anything that speaks MCP over HTTP) with a private Discord form, and the host gets the servers and a routing description for each, to give its agents.
|
|
7
|
+
[ContextForge](#contextforge) holds every upstream server and its token.
|
|
8
|
+
- `remoteMcp`: an MCP server over HTTP for agents outside Discord.
|
|
9
|
+
One endpoint relays turns to the owner's agent and returns the answer when polled.
|
|
10
|
+
Another offers Discord channel tools, only for the channels the owner granted on Discord.
|
|
11
|
+
|
|
12
|
+
Bun only, like pi-roundtable.
|
|
13
|
+
The package ships its TypeScript source, so there is no build step.
|
|
14
|
+
MIT licensed.
|
|
15
|
+
|
|
16
|
+
## What you need
|
|
17
|
+
|
|
18
|
+
- [Bun](https://bun.sh/docs/installation) 1.3 or newer.
|
|
19
|
+
- A running pi-roundtable host, version 0.4 (`pi-roundtable` is a peer dependency, `^0.4.0`), with its PostgreSQL.
|
|
20
|
+
- For `mcpConnectors`: a [ContextForge](#contextforge) gateway that you run yourself.
|
|
21
|
+
- For `remoteMcp`: an HTTPS address that reaches the host's `public` listener, such as a tunnel; this is the host's `http.publicUrl`.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
bun add pi-roundtable-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configure
|
|
30
|
+
|
|
31
|
+
List the plugins in `roundtable.config.ts`.
|
|
32
|
+
Both read the Discord connection, so they go after the built-in plugins, which is where `plugins` puts them.
|
|
33
|
+
Credentials come from the environment; nothing secret belongs in the file.
|
|
34
|
+
|
|
35
|
+
<!-- example: examples/roundtable.config.ts -->
|
|
36
|
+
```ts
|
|
37
|
+
import type { RoundtableConfig } from "pi-roundtable";
|
|
38
|
+
import { mcpConnectors, remoteMcp } from "pi-roundtable-mcp";
|
|
39
|
+
|
|
40
|
+
// Credentials come from .env, which Bun loads on its own; nothing secret belongs in this file.
|
|
41
|
+
const env = (name: string): string => process.env[name] ?? "";
|
|
42
|
+
|
|
43
|
+
export default {
|
|
44
|
+
name: "Roundtable",
|
|
45
|
+
owner: { id: env("OWNER_ID"), name: env("OWNER_NAME") },
|
|
46
|
+
discord: {
|
|
47
|
+
token: env("DISCORD_TOKEN"),
|
|
48
|
+
guild: env("DISCORD_GUILD_ID"),
|
|
49
|
+
entryChannel: env("DISCORD_ENTRY_CHANNEL_ID"),
|
|
50
|
+
},
|
|
51
|
+
database: { url: env("DATABASE_URL") },
|
|
52
|
+
dataDir: "./data",
|
|
53
|
+
model: env("MODEL"),
|
|
54
|
+
// The remote MCP endpoints are served on this listener, so PUBLIC_URL must reach it over HTTPS.
|
|
55
|
+
http: { publicUrl: env("PUBLIC_URL") },
|
|
56
|
+
plugins: [
|
|
57
|
+
mcpConnectors({
|
|
58
|
+
contextForge: {
|
|
59
|
+
url: env("CONTEXTFORGE_URL"),
|
|
60
|
+
jwtSecret: env("CONTEXTFORGE_JWT_SECRET"),
|
|
61
|
+
user: env("CONTEXTFORGE_USER"),
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
remoteMcp({
|
|
65
|
+
dispatchToken: env("MCP_DISPATCH_TOKEN"),
|
|
66
|
+
publicUrl: env("PUBLIC_URL"),
|
|
67
|
+
}),
|
|
68
|
+
],
|
|
69
|
+
} satisfies RoundtableConfig;
|
|
70
|
+
```
|
|
71
|
+
<!-- /example -->
|
|
72
|
+
|
|
73
|
+
### `mcpConnectors(options)`
|
|
74
|
+
|
|
75
|
+
| Option | Type | Default | What it is |
|
|
76
|
+
| --- | --- | --- | --- |
|
|
77
|
+
| `contextForge.url` | `string` | required | The gateway's base URL, such as `http://localhost:4444` |
|
|
78
|
+
| `contextForge.jwtSecret` | `string` | required | The secret ContextForge signs and verifies its JWTs with (its `JWT_SECRET_KEY`) |
|
|
79
|
+
| `contextForge.user` | `string` | required | The admin user the plugin acts as, an email address |
|
|
80
|
+
| `serverPrefix` | `string` | `roundtable-conn-` | The start of every connector's virtual server name in ContextForge |
|
|
81
|
+
| `maxToolName` | `number` | `45` | Tools whose names are longer are left out of a connector's server, since the model API limits a tool name (64 characters for Claude) and a client may add a prefix of its own |
|
|
82
|
+
| `messages` | `Partial<ConnectorMessages>` | English | The Discord text and the registry's refusals, in your wording |
|
|
83
|
+
|
|
84
|
+
It provides the `CONNECTORS` service (`Connectors`):
|
|
85
|
+
|
|
86
|
+
| Member | What it is |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `version` | A number that changes on every add, change of purpose, and removal |
|
|
89
|
+
| `list()` | Every connector: name, URL, purpose, and its virtual server when it could be read |
|
|
90
|
+
| `servers()` | The virtual servers (name, URL, tool names) of the connectors that could be read |
|
|
91
|
+
| `profileSources()` | One `{ name, description, serverName }` per connector, the shape a host builds a per-agent MCP profile from |
|
|
92
|
+
| `token` | The bearer token that the virtual servers' URLs expect; one per process, valid for a year |
|
|
93
|
+
|
|
94
|
+
The plugin does not decide which agent uses which connector.
|
|
95
|
+
The host reads the service, builds its own profiles, and compares `version` with the one it built at.
|
|
96
|
+
A plugin registered after `mcpConnectors` can read the list like this:
|
|
97
|
+
|
|
98
|
+
<!-- example: examples/connector-profiles.ts -->
|
|
99
|
+
```ts
|
|
100
|
+
import { definePlugin } from "pi-roundtable";
|
|
101
|
+
import { CONNECTORS } from "pi-roundtable-mcp";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A plugin registered after `mcpConnectors` reads the owner's connectors from the service and builds
|
|
105
|
+
* whatever it needs from them: here, one line per connector for its own routing prompt. `version`
|
|
106
|
+
* changes on every add, change, and removal, so a cache of anything built from the list is stale
|
|
107
|
+
* when the version it was built at is not the current one.
|
|
108
|
+
*/
|
|
109
|
+
export function connectorRouting(onLines: (lines: string[]) => void) {
|
|
110
|
+
return definePlugin({
|
|
111
|
+
name: "connector-routing",
|
|
112
|
+
requires: [CONNECTORS],
|
|
113
|
+
setup: ({ services }) => {
|
|
114
|
+
const connectors = services.get(CONNECTORS);
|
|
115
|
+
let builtAt = -1;
|
|
116
|
+
let lines: string[] = [];
|
|
117
|
+
return {
|
|
118
|
+
services: [
|
|
119
|
+
{
|
|
120
|
+
name: "connector-routing",
|
|
121
|
+
start: () => {
|
|
122
|
+
if (builtAt !== connectors.version) {
|
|
123
|
+
lines = connectors
|
|
124
|
+
.profileSources()
|
|
125
|
+
.map((source) => `${source.name}: ${source.description}`);
|
|
126
|
+
builtAt = connectors.version;
|
|
127
|
+
}
|
|
128
|
+
onLines(lines);
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
<!-- /example -->
|
|
138
|
+
|
|
139
|
+
The owner manages connectors on Discord with `/<root> connector`, where `<root>` is the host's root command:
|
|
140
|
+
|
|
141
|
+
| Command | What it does |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `add` | Opens a private form: name, MCP URL, purpose, and an optional token (sent as `Authorization: Bearer`, or under a header name you give). ContextForge stores the token encrypted; Discord shows the form's values to nobody else |
|
|
144
|
+
| `list` | Lists the connectors with their tools; the URL is shown as scheme and host only, since some servers keep a credential in the path |
|
|
145
|
+
| `describe` | Changes a connector's purpose |
|
|
146
|
+
| `remove` | Deletes the connector, its virtual server, and the upstream server with its token |
|
|
147
|
+
|
|
148
|
+
### `remoteMcp(options)`
|
|
149
|
+
|
|
150
|
+
| Option | Type | Default | What it is |
|
|
151
|
+
| --- | --- | --- | --- |
|
|
152
|
+
| `dispatchToken` | `string` | required | The bearer token an outside agent presents at `/mcp/personal`. Make it long and keep it secret |
|
|
153
|
+
| `publicUrl` | `string` | required | The HTTPS address that reaches the host's `public` listener. Granted-channel URLs are built on its origin |
|
|
154
|
+
| `persona` | `string` | a short neutral prompt | The system prompt of the default `remote` conversations |
|
|
155
|
+
| `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 |
|
|
157
|
+
|
|
158
|
+
The plugin serves two endpoints on the host's `public` listener:
|
|
159
|
+
|
|
160
|
+
| Endpoint | Who may call it | What it offers |
|
|
161
|
+
| --- | --- | --- |
|
|
162
|
+
| `POST /mcp/personal` | An agent that sends `Authorization: Bearer <dispatchToken>` | `agent_dispatch` starts or continues a conversation and returns `{ runId, sessionId }` at once. `agent_result` polls a run: `working`, `completed` with the text, or `failed` |
|
|
163
|
+
| `POST /mcp/discord/<token>` | An agent that holds a bundle's URL | `discord_list_authorized_channels`, and the Discord tools for exactly the operations the bundle's channels were granted |
|
|
164
|
+
|
|
165
|
+
Each request gets a fresh stateless MCP server.
|
|
166
|
+
A browser request (one with an `Origin` header) is refused, and so is a body over 12 MiB.
|
|
167
|
+
A run that takes more than 30 minutes is reported failed, and a finished run can be polled for an hour.
|
|
168
|
+
|
|
169
|
+
The owner grants channels on Discord with `/<root> mcp`:
|
|
170
|
+
|
|
171
|
+
| Command | What it does |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| `authorize` | Run it in the channel to grant. Adds the channel to a bundle (created on first use) and asks which operations to allow: `read`, `send`, `edit`, `pin`, `delete`, `channel`, `permissions`. The first grant of a bundle shows its MCP URL once |
|
|
174
|
+
| `grants` | Lists every bundle with its channels, and this channel's recent audit entries |
|
|
175
|
+
| `revoke` | Removes a channel from a bundle |
|
|
176
|
+
| `describe` | Changes the channel name and purpose that the agent sees (Discord's own name and topic stay) |
|
|
177
|
+
| `token` | Replaces a bundle's MCP URL; the old one stops working at once and the channel settings stay |
|
|
178
|
+
|
|
179
|
+
#### The default conversation
|
|
180
|
+
|
|
181
|
+
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.
|
|
182
|
+
Nothing is posted to Discord, since no chat surface serves `mcp:` channels; the outside agent polls for the answer.
|
|
183
|
+
A relayed message that the host's judge reads as approving held actions confirms them, as the owner's own reply would.
|
|
184
|
+
Each message begins with a short note that says the owner wrote it in an outside agent.
|
|
185
|
+
Starting a remote conversation over archives it, and deleting it also ends the outside agent's session.
|
|
186
|
+
Sessions that stay idle for 14 days are deleted once a day.
|
|
187
|
+
|
|
188
|
+
#### When the host runs the conversations itself
|
|
189
|
+
|
|
190
|
+
A host that already has its own owner conversations (its own routing, its own prompt) gives `answer` and `claim` together:
|
|
191
|
+
|
|
192
|
+
<!-- example: examples/host-conversation.ts -->
|
|
193
|
+
```ts
|
|
194
|
+
import { remoteMcp } from "pi-roundtable-mcp";
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A host that runs the owner's conversations itself passes `answer` and `claim` together. `answer`
|
|
198
|
+
* runs one turn and never rejects; it joins the channel's queue itself. `claim` says what the
|
|
199
|
+
* claim over the `mcp:<session>` channels does with those conversations.
|
|
200
|
+
*/
|
|
201
|
+
export function hostRemote(dispatchToken: string, publicUrl: string) {
|
|
202
|
+
return remoteMcp({
|
|
203
|
+
dispatchToken,
|
|
204
|
+
publicUrl,
|
|
205
|
+
answer: async (channel, text) => ({
|
|
206
|
+
ok: true,
|
|
207
|
+
text: `Answered ${text.length} characters in ${channel}.`,
|
|
208
|
+
}),
|
|
209
|
+
claim: {
|
|
210
|
+
// The string names whose conversation it was: the host's own kind.
|
|
211
|
+
startFresh: async () => "owner",
|
|
212
|
+
deleteConversation: async () => undefined,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
<!-- /example -->
|
|
218
|
+
|
|
219
|
+
- `answer(channel, text)` runs one turn and never rejects.
|
|
220
|
+
The plugin does not queue it, so it joins the channel's queue itself (`context.queue.run`).
|
|
221
|
+
The host owns the conversations' kind and persona, so the plugin contributes no persona in this mode.
|
|
222
|
+
- `claim` is what the claim over the `mcp:<session>` channels does with those conversations: `startFresh` (required; the string it returns is the conversation's kind), `deleteConversation` (required; the plugin removes the session record after it), and optional `stop` and `background`.
|
|
223
|
+
A claim without `background` skips background turns.
|
|
224
|
+
|
|
225
|
+
## The grant and security model
|
|
226
|
+
|
|
227
|
+
- **The dispatch token is the owner's voice.** Whoever holds it can send the owner's agent messages as the owner (owner tier, with every tool the owner's agent has), and can approve its held actions by saying so. Keep it secret, change it by changing the option, and serve the endpoint only over HTTPS.
|
|
228
|
+
- **A bundle URL is a credential.** It carries 32 random bytes; only its SHA-256 hash is stored, and the URL is shown once, when the bundle is created or replaced. Everyone holding it can use every channel in the bundle with the operations granted there, and nothing else.
|
|
229
|
+
- **The owner approves each grant on Discord.** Only the owner can use the commands. A grant needs the owner to hold *Manage Channels* in the channel, and both the owner and the bot to hold the Discord permissions behind every operation chosen, so a grant never exceeds what both may do. The pending choice expires after five minutes.
|
|
230
|
+
- **Every call is checked again.** The grant, the guild, and the bundle's token are read after Discord is inspected, so a revoke or a replaced URL stops a call in flight. Each call is written to an audit table before it runs and marked afterwards.
|
|
231
|
+
- **Failures say little.** An outside agent gets an error code, never a stack trace or the URL; a failed run shows a fixed message, and the details go to the host's log. Messages in channels are untrusted data, and the tool descriptions say so.
|
|
232
|
+
|
|
233
|
+
## ContextForge
|
|
234
|
+
|
|
235
|
+
[ContextForge](https://github.com/IBM/mcp-context-forge) is IBM's open-source MCP gateway: it federates MCP servers behind one endpoint, keeps each upstream server's credentials, and lets you expose chosen tools as a *virtual server*.
|
|
236
|
+
`mcpConnectors` uses it through its admin API (`/gateways`, `/tools`, `/servers`), signing a short admin JWT with `contextForge.jwtSecret`.
|
|
237
|
+
**This package does not run ContextForge.**
|
|
238
|
+
Run it yourself, next to the host, following its own [quick start](https://github.com/IBM/mcp-context-forge#quick-start---containers), with the same `JWT_SECRET_KEY` that you give the plugin, and point `contextForge.url` at it.
|
|
239
|
+
The host's agents then reach a connector through the connector's virtual server URL, with `CONNECTORS.token` as the bearer token.
|
|
240
|
+
|
|
241
|
+
## Wording
|
|
242
|
+
|
|
243
|
+
pi-roundtable's message catalog is closed to plugins, so these plugins keep their text in English and take a `messages` option of their own: a partial object laid over the defaults.
|
|
244
|
+
Entries that take values are functions.
|
|
245
|
+
The types `ConnectorMessages` and `RemoteMcpMessages` are exported and list every key.
|
|
246
|
+
Operation labels (`read`, `send`, ...) come from pi-roundtable and follow the host's language.
|
|
247
|
+
The errors of the ContextForge client (a refused request) are English only.
|
|
248
|
+
|
|
249
|
+
## Database
|
|
250
|
+
|
|
251
|
+
Each plugin declares its migrations; the host runs them before any setup.
|
|
252
|
+
Tables: `owner_connectors`; `discord_mcp_bundles`, `discord_channel_grants`, `discord_channel_audit`, `remote_agent_sessions`.
|
|
253
|
+
|
|
254
|
+
## Development
|
|
255
|
+
|
|
256
|
+
```sh
|
|
257
|
+
bun install
|
|
258
|
+
bun run typecheck
|
|
259
|
+
bun run lint
|
|
260
|
+
bun test
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Tests that need PostgreSQL are skipped unless `ROUNDTABLE_TEST_DATABASE_URL` points at a throwaway test database:
|
|
264
|
+
|
|
265
|
+
```sh
|
|
266
|
+
ROUNDTABLE_TEST_DATABASE_URL=postgres://postgres:postgres@localhost:5432/roundtable_test bun test
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
CI runs them against a PostgreSQL service.
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
MIT
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { definePlugin } from "pi-roundtable";
|
|
2
|
+
import { CONNECTORS } from "pi-roundtable-mcp";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A plugin registered after `mcpConnectors` reads the owner's connectors from the service and builds
|
|
6
|
+
* whatever it needs from them: here, one line per connector for its own routing prompt. `version`
|
|
7
|
+
* changes on every add, change, and removal, so a cache of anything built from the list is stale
|
|
8
|
+
* when the version it was built at is not the current one.
|
|
9
|
+
*/
|
|
10
|
+
export function connectorRouting(onLines: (lines: string[]) => void) {
|
|
11
|
+
return definePlugin({
|
|
12
|
+
name: "connector-routing",
|
|
13
|
+
requires: [CONNECTORS],
|
|
14
|
+
setup: ({ services }) => {
|
|
15
|
+
const connectors = services.get(CONNECTORS);
|
|
16
|
+
let builtAt = -1;
|
|
17
|
+
let lines: string[] = [];
|
|
18
|
+
return {
|
|
19
|
+
services: [
|
|
20
|
+
{
|
|
21
|
+
name: "connector-routing",
|
|
22
|
+
start: () => {
|
|
23
|
+
if (builtAt !== connectors.version) {
|
|
24
|
+
lines = connectors
|
|
25
|
+
.profileSources()
|
|
26
|
+
.map((source) => `${source.name}: ${source.description}`);
|
|
27
|
+
builtAt = connectors.version;
|
|
28
|
+
}
|
|
29
|
+
onLines(lines);
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { remoteMcp } from "pi-roundtable-mcp";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A host that runs the owner's conversations itself passes `answer` and `claim` together. `answer`
|
|
5
|
+
* runs one turn and never rejects; it joins the channel's queue itself. `claim` says what the
|
|
6
|
+
* claim over the `mcp:<session>` channels does with those conversations.
|
|
7
|
+
*/
|
|
8
|
+
export function hostRemote(dispatchToken: string, publicUrl: string) {
|
|
9
|
+
return remoteMcp({
|
|
10
|
+
dispatchToken,
|
|
11
|
+
publicUrl,
|
|
12
|
+
answer: async (channel, text) => ({
|
|
13
|
+
ok: true,
|
|
14
|
+
text: `Answered ${text.length} characters in ${channel}.`,
|
|
15
|
+
}),
|
|
16
|
+
claim: {
|
|
17
|
+
// The string names whose conversation it was: the host's own kind.
|
|
18
|
+
startFresh: async () => "owner",
|
|
19
|
+
deleteConversation: async () => undefined,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RoundtableConfig } from "pi-roundtable";
|
|
2
|
+
import { mcpConnectors, remoteMcp } from "pi-roundtable-mcp";
|
|
3
|
+
|
|
4
|
+
// Credentials come from .env, which Bun loads on its own; nothing secret belongs in this file.
|
|
5
|
+
const env = (name: string): string => process.env[name] ?? "";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
name: "Roundtable",
|
|
9
|
+
owner: { id: env("OWNER_ID"), name: env("OWNER_NAME") },
|
|
10
|
+
discord: {
|
|
11
|
+
token: env("DISCORD_TOKEN"),
|
|
12
|
+
guild: env("DISCORD_GUILD_ID"),
|
|
13
|
+
entryChannel: env("DISCORD_ENTRY_CHANNEL_ID"),
|
|
14
|
+
},
|
|
15
|
+
database: { url: env("DATABASE_URL") },
|
|
16
|
+
dataDir: "./data",
|
|
17
|
+
model: env("MODEL"),
|
|
18
|
+
// The remote MCP endpoints are served on this listener, so PUBLIC_URL must reach it over HTTPS.
|
|
19
|
+
http: { publicUrl: env("PUBLIC_URL") },
|
|
20
|
+
plugins: [
|
|
21
|
+
mcpConnectors({
|
|
22
|
+
contextForge: {
|
|
23
|
+
url: env("CONTEXTFORGE_URL"),
|
|
24
|
+
jwtSecret: env("CONTEXTFORGE_JWT_SECRET"),
|
|
25
|
+
user: env("CONTEXTFORGE_USER"),
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
remoteMcp({
|
|
29
|
+
dispatchToken: env("MCP_DISPATCH_TOKEN"),
|
|
30
|
+
publicUrl: env("PUBLIC_URL"),
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
} satisfies RoundtableConfig;
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-roundtable-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP connectors through ContextForge and a remote MCP endpoint for pi-roundtable",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"bun": ">=1.3.0"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"!src/**/*.test.ts",
|
|
16
|
+
"!src/testing",
|
|
17
|
+
"examples",
|
|
18
|
+
"!examples/**/*.test.ts",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/wayne930242/pi-roundtable-mcp.git"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"provenance": true
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"lint": "biome check .",
|
|
34
|
+
"test": "bun test"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@modelcontextprotocol/sdk": "1.31.0",
|
|
38
|
+
"discord.js": "14.27.0",
|
|
39
|
+
"typebox": "1.3.34"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"pi-roundtable": "^0.4.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@biomejs/biome": "2.5.15",
|
|
46
|
+
"@types/bun": "1.4.2",
|
|
47
|
+
"pi-roundtable": "0.4.0",
|
|
48
|
+
"typescript": "7.0.2"
|
|
49
|
+
}
|
|
50
|
+
}
|