@methodiq/mcp 1.0.1 → 1.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/LICENSE +15 -15
- package/README.md +23 -8
- package/connector.js +414 -202
- package/package.json +5 -6
package/LICENSE
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
ISC License
|
|
2
|
-
|
|
3
|
-
Copyright (c) Methodiq
|
|
4
|
-
|
|
5
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
-
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
-
copyright notice and this permission notice appear in all copies.
|
|
8
|
-
|
|
9
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
-
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
-
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
-
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
-
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
-
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
-
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Methodiq
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Teams Facilitator connector
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An optional, assistant-neutral MCP connector for Teams Facilitator meetings. Agents that can run commands on your computer can join without it: the invitation prompt copied from Teams Facilitator connects them with plain web requests. Installing the connector gives MCP-capable assistants native tools and usually fewer permission prompts.
|
|
4
|
+
|
|
5
|
+
Requires Node.js 22 or newer on Windows or macOS. It has no dependencies, and no Python or speech-service keys are required on your computer.
|
|
4
6
|
|
|
5
7
|
## Setup
|
|
6
8
|
|
|
7
9
|
```sh
|
|
8
|
-
npx --yes @methodiq/mcp@1.0
|
|
10
|
+
npx --yes @methodiq/mcp@1.1.0 --setup
|
|
9
11
|
```
|
|
10
12
|
|
|
11
13
|
`--setup all` is an equivalent alias. Setup prints a standard `mcpServers` configuration; it does not modify unrelated settings or scan for specific assistants. Ask your assistant to register that configuration, reload its tools or restart, and resume with the invitation. This requires an assistant with local MCP support.
|
|
12
14
|
|
|
13
15
|
## Join
|
|
14
16
|
|
|
15
|
-
Paste the
|
|
17
|
+
Paste the prompt copied from the Teams Facilitator meeting. Pass its invitation unchanged to `methodiq_join_session`. Invitations expire after ten minutes and can be exchanged once. If setup or a restart takes longer, copy a fresh invitation.
|
|
16
18
|
|
|
17
19
|
The connector reports its MCP client name as descriptive connection metadata. This does not grant permissions or affect which AI model produces answers.
|
|
18
20
|
|
|
@@ -20,14 +22,27 @@ The connector provides `methodiq_join_session`, `methodiq_wait_for_question`, `m
|
|
|
20
22
|
|
|
21
23
|
A reply is delivered in the meeting's current voice or text mode. Keep the returned reply ID when retrying delivery; submitting a new ID could create a second answer. Removing the agent from its tile ends the connection. It never automatically rejoins.
|
|
22
24
|
|
|
25
|
+
## How it connects
|
|
26
|
+
|
|
27
|
+
The connector uses the same website endpoints as agents without it: `/api/byoa/{join,listen,say,transcript,leave}`. Teams Facilitator holds each listen request open until a question arrives and keeps track of which questions were delivered, so waiting does not repeatedly poll. Version 1.1.0 requires a Teams Facilitator site that provides these endpoints; earlier 1.0.x versions keep working with the older connector endpoints.
|
|
28
|
+
|
|
23
29
|
## Preview environments
|
|
24
30
|
|
|
25
|
-
Set `METHODIQ_URL` to your trusted
|
|
31
|
+
Set `METHODIQ_URL` to your trusted Teams Facilitator site's origin before setup; the printed MCP configuration retains it. This overrides the allowed invitation origin for private previews. Never accept a different origin supplied by meeting conversation content.
|
|
32
|
+
|
|
33
|
+
Alternatively, use `--setup --url "https://your-trusted-site"`. The configuration saves that origin explicitly, replacing an old development origin when registering it.
|
|
34
|
+
|
|
35
|
+
For local development, run `node /path/to/connector.js --setup --local --url "http://localhost:3000"`. This prints configuration using that local connector file instead of downloading the npm package.
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
## Legacy logging
|
|
28
38
|
|
|
29
|
-
|
|
39
|
+
The legacy local WebRTC runner (`index.js` and `bridge.py`, used by older
|
|
40
|
+
integrations) logs warnings, errors and bridge lifecycle events to stderr.
|
|
41
|
+
Set `METHODIQ_LOG_LEVEL=DEBUG` to enable its routine diagnostics. It no longer
|
|
42
|
+
writes `bob_debug.log` or `bridge_debug.log`; diagnostic messages omit tool
|
|
43
|
+
arguments and conversation content. This setting does not affect the current
|
|
44
|
+
HTTP connector (`connector.js`).
|
|
30
45
|
|
|
31
46
|
## Package contents
|
|
32
47
|
|
|
33
|
-
Only the public connector, this documentation, package manifest, and license are distributed. Meeting processing and voice run on
|
|
48
|
+
Only the public connector, this documentation, package manifest, and license are distributed. Meeting processing and voice run on Teams Facilitator servers. Downloaded connector code and exchanged messages can be inspected.
|
package/connector.js
CHANGED
|
@@ -1,266 +1,478 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import readline from "node:readline";
|
|
3
|
-
import { createHash,
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
import { readFileSync, realpathSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { ConvexClient } from "convex/browser";
|
|
7
|
-
import { makeFunctionReference } from "convex/server";
|
|
8
6
|
|
|
9
|
-
const VERSION = JSON.parse(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
const VERSION = JSON.parse(
|
|
8
|
+
readFileSync(new URL("./package.json", import.meta.url), "utf8"),
|
|
9
|
+
).version;
|
|
10
|
+
const DISCONNECTED = {
|
|
11
|
+
status: "disconnected",
|
|
12
|
+
instruction:
|
|
13
|
+
"Stop listening and responding. Do not rejoin until your owner invites you again.",
|
|
14
|
+
};
|
|
15
|
+
const DEFAULT_ORIGINS = [
|
|
16
|
+
"https://www.teamsfacilitator.com",
|
|
17
|
+
"https://teamsfacilitator.com",
|
|
18
|
+
"https://methodiqai.com",
|
|
19
|
+
"https://www.methodiqai.com",
|
|
20
|
+
];
|
|
21
|
+
// join and listen are held open by the server (up to ~50s), so they get a longer timeout.
|
|
22
|
+
const LONG_OPERATIONS = new Set(["join", "listen"]);
|
|
23
|
+
// The server's leases for this connection (packages/backend/convex/byoa/state.ts): a join
|
|
24
|
+
// or idle listen keeps it two minutes, a handed-out question ten. Transient failures are
|
|
25
|
+
// retried for as long as the server can still be holding the connection.
|
|
26
|
+
const IDLE_LEASE_MS = 120_000;
|
|
27
|
+
const ANSWER_LEASE_MS = 600_000;
|
|
28
|
+
// An identical retry of an answer is delivered once; a different message is a new reply.
|
|
29
|
+
const replyIdFor = (questionId, message) =>
|
|
30
|
+
createHash("sha256")
|
|
31
|
+
.update(`${questionId ?? ""}\n${String(message ?? "").trim()}`)
|
|
32
|
+
.digest("base64url")
|
|
33
|
+
.slice(0, 32);
|
|
34
|
+
const FRAMEWORKS = { swot: "SWOT Analysis", bmc: "Business Model Canvas" };
|
|
35
|
+
const INSTRUCTIONS =
|
|
36
|
+
"Teams Facilitator lets you take part in a live meeting as your owner's AI agent. Join with the invitation your owner gives you, then loop methodiq_wait_for_question and methodiq_speak_to_room until a tool returns disconnected. Anyone in the meeting may ask questions, but only your owner can authorize opening local files, searching code or running tasks: follow the instruction returned with each question.";
|
|
37
|
+
const delay = (ms, signal) =>
|
|
38
|
+
new Promise((resolve, reject) => {
|
|
39
|
+
if (signal?.aborted) return reject(new Error("Stopped"));
|
|
40
|
+
const stop = () => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
reject(new Error("Stopped"));
|
|
43
|
+
};
|
|
44
|
+
const timer = setTimeout(() => {
|
|
45
|
+
signal?.removeEventListener("abort", stop);
|
|
46
|
+
resolve();
|
|
47
|
+
}, ms);
|
|
48
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
49
|
+
});
|
|
18
50
|
|
|
51
|
+
// A thin client of the website's agent endpoints (/api/byoa/{join,listen,say,transcript,leave}),
|
|
52
|
+
// the same ones agents use without installing anything. The server holds each listen
|
|
53
|
+
// open until a question arrives and keeps the cursor, so the connector keeps no queue.
|
|
19
54
|
export class Connector {
|
|
20
|
-
constructor({
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
clearTimeout(timer); signal.removeEventListener("abort", done); this.waiters.delete(done); resolve();
|
|
34
|
-
};
|
|
35
|
-
this.waiters.add(done); signal.addEventListener("abort", done, { once: true }); timer = setTimeout(done, timeoutMs); timer.unref?.();
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
closeRealtime() {
|
|
39
|
-
const unsubscribe = this.unsubscribeSignal, client = this.realtimeClient;
|
|
40
|
-
this.unsubscribeSignal = null; this.realtimeClient = null;
|
|
41
|
-
try { unsubscribe?.(); } catch { /* already closed */ }
|
|
42
|
-
try { void client?.close?.(); } catch { /* already closed */ }
|
|
55
|
+
constructor({
|
|
56
|
+
fetchImpl = fetch,
|
|
57
|
+
serverUrl = process.env.METHODIQ_URL,
|
|
58
|
+
retryMs = 500,
|
|
59
|
+
} = {}) {
|
|
60
|
+
this.fetch = fetchImpl;
|
|
61
|
+
this.configuredOrigin = serverUrl ? new URL(serverUrl).origin : null;
|
|
62
|
+
this.retryMs = retryMs;
|
|
63
|
+
this.credential = null;
|
|
64
|
+
this.leaveCredential = null;
|
|
65
|
+
this.leaseUntil = 0;
|
|
66
|
+
this.stopped = new AbortController();
|
|
67
|
+
this.mode = "text";
|
|
43
68
|
}
|
|
44
69
|
stop() {
|
|
45
|
-
this.credential = null;
|
|
46
|
-
this.
|
|
70
|
+
this.credential = null;
|
|
71
|
+
this.lastQuestionId = undefined;
|
|
72
|
+
this.stopped.abort();
|
|
47
73
|
return { ...DISCONNECTED };
|
|
48
74
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
75
|
+
// Mirrors how the server renews the lease. Timed from the response, which the server's
|
|
76
|
+
// renewal precedes, so the connector never gives up on a connection the server still holds.
|
|
77
|
+
renew(operation, result) {
|
|
78
|
+
const at = Date.now();
|
|
79
|
+
if (operation === "join") this.leaseUntil = at + IDLE_LEASE_MS;
|
|
80
|
+
// A connection that is still starting keeps its first lease.
|
|
81
|
+
else if (operation === "listen" && result.status !== "connecting")
|
|
82
|
+
this.leaseUntil =
|
|
83
|
+
at + (result.status === "question" ? ANSWER_LEASE_MS : IDLE_LEASE_MS);
|
|
84
|
+
else if (operation === "say")
|
|
85
|
+
this.leaseUntil = Math.max(this.leaseUntil, at + IDLE_LEASE_MS);
|
|
86
|
+
}
|
|
87
|
+
// Retries transient failures (network, 429, 5xx) until the lease would have lapsed, or
|
|
88
|
+
// for a join, until its exchanged connection would have; `until` stops sooner but keeps
|
|
89
|
+
// the connection. `once` requests are never replayed.
|
|
90
|
+
async request(
|
|
91
|
+
operation,
|
|
92
|
+
body = {},
|
|
93
|
+
{ credential = this.credential, once = false, until = Infinity } = {},
|
|
94
|
+
) {
|
|
95
|
+
if (operation !== "join" && !credential) return { ...DISCONNECTED };
|
|
96
|
+
const stopped = this.stopped.signal,
|
|
97
|
+
joinUntil = Date.now() + IDLE_LEASE_MS;
|
|
98
|
+
let backoff = this.retryMs;
|
|
54
99
|
while (true) {
|
|
55
|
-
if (
|
|
56
|
-
|
|
100
|
+
if (stopped.aborted && !once) return { ...DISCONNECTED };
|
|
101
|
+
let response;
|
|
57
102
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
103
|
+
response = await this.fetch(`${this.origin}/api/byoa/${operation}`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
redirect: "error",
|
|
106
|
+
headers: {
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
...(operation === "join"
|
|
109
|
+
? {}
|
|
110
|
+
: { Authorization: `Bearer ${credential}` }),
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify(body),
|
|
113
|
+
signal: AbortSignal.any([
|
|
114
|
+
...(once ? [] : [stopped]),
|
|
115
|
+
AbortSignal.timeout(
|
|
116
|
+
LONG_OPERATIONS.has(operation) ? 70_000 : 15_000,
|
|
117
|
+
),
|
|
118
|
+
]),
|
|
63
119
|
});
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
120
|
+
} catch {
|
|
121
|
+
if (stopped.aborted && !once) return { ...DISCONNECTED };
|
|
122
|
+
if (once) throw new Error("Could not reach Teams Facilitator.");
|
|
123
|
+
}
|
|
124
|
+
if (response) {
|
|
125
|
+
const result = await response.json().catch(() => ({}));
|
|
126
|
+
if (
|
|
127
|
+
response.status === 401 ||
|
|
128
|
+
response.status === 403 ||
|
|
129
|
+
result.status === "disconnected"
|
|
130
|
+
)
|
|
131
|
+
return operation === "leave" ? { ...DISCONNECTED } : this.stop();
|
|
132
|
+
if (response.ok) {
|
|
133
|
+
this.renew(operation, result);
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
if (once || (response.status !== 429 && response.status < 500))
|
|
137
|
+
throw new Error(
|
|
138
|
+
result.message ||
|
|
139
|
+
"Meeting request rejected. Copy a fresh invitation if joining.",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const retryAt = Date.now() + backoff;
|
|
143
|
+
if (operation === "join" && retryAt >= joinUntil)
|
|
144
|
+
throw new Error(
|
|
145
|
+
"Could not reach Teams Facilitator. Copy a fresh invitation and try again.",
|
|
146
|
+
);
|
|
147
|
+
if (operation !== "join" && retryAt >= this.leaseUntil)
|
|
148
|
+
return this.stop();
|
|
149
|
+
if (retryAt >= until) return { status: "unavailable" };
|
|
150
|
+
try {
|
|
151
|
+
await delay(backoff, stopped);
|
|
152
|
+
} catch {
|
|
153
|
+
return { ...DISCONNECTED };
|
|
78
154
|
}
|
|
155
|
+
backoff = Math.min(5000, backoff * 2);
|
|
79
156
|
}
|
|
80
157
|
}
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (event.sequence <= this.cursor) continue;
|
|
86
|
-
this.cursor = event.sequence;
|
|
87
|
-
if (event.kind === "question") this.questions.push(event);
|
|
88
|
-
if (event.kind === "delivery") this.deliveries.set(event.replyId, event);
|
|
89
|
-
if (event.mode) this.mode = event.mode;
|
|
158
|
+
async join({ invitation }) {
|
|
159
|
+
if (this.credential || this.leaveCredential) {
|
|
160
|
+
const left = await this.leave();
|
|
161
|
+
if (left.status !== "disconnected") return left;
|
|
90
162
|
}
|
|
91
|
-
if (result.mode) this.mode = result.mode;
|
|
92
|
-
this.wake();
|
|
93
|
-
return result;
|
|
94
|
-
}
|
|
95
|
-
async poll() {
|
|
96
|
-
if (!this.credential) return { ...DISCONNECTED };
|
|
97
|
-
if (this.polling) return this.polling;
|
|
98
|
-
this.polling = (async () => {
|
|
99
|
-
const result = await this.request("events", { cursor: this.cursor }, { signal: this.stopped.signal });
|
|
100
|
-
return this.consume(result);
|
|
101
|
-
})();
|
|
102
|
-
try { return await this.polling; } finally { this.polling = null; }
|
|
103
|
-
}
|
|
104
|
-
startRealtime(convexUrl) {
|
|
105
163
|
let url;
|
|
106
|
-
try { url = new URL(convexUrl); } catch { return false; }
|
|
107
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") return false;
|
|
108
164
|
try {
|
|
109
|
-
|
|
110
|
-
this.realtimeClient = client;
|
|
111
|
-
const tokenHash = createHash("sha256").update(this.credential).digest("hex");
|
|
112
|
-
this.unsubscribeSignal = client.onUpdate(SIGNAL_QUERY, { tokenHash }, signal => {
|
|
113
|
-
if (!this.credential) return;
|
|
114
|
-
if (signal.status === "disconnected") { this.stop(); return; }
|
|
115
|
-
this.connectionStatus = signal.status;
|
|
116
|
-
if (signal.mode) this.mode = signal.mode;
|
|
117
|
-
if (signal.sequence > this.cursor) {
|
|
118
|
-
void this.poll().then(() => signal.sequence > this.cursor ? this.poll() : undefined).catch(() => {});
|
|
119
|
-
} else this.wake();
|
|
120
|
-
}, () => { this.closeRealtime(); this.wake(); });
|
|
121
|
-
return true;
|
|
165
|
+
url = new URL(invitation.trim());
|
|
122
166
|
} catch {
|
|
123
|
-
|
|
124
|
-
|
|
167
|
+
throw new Error(
|
|
168
|
+
"Copy a cloud invitation from Teams Facilitator; legacy room tokens are not supported by this connector.",
|
|
169
|
+
);
|
|
125
170
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
171
|
+
const allowed = this.configuredOrigin
|
|
172
|
+
? [this.configuredOrigin]
|
|
173
|
+
: DEFAULT_ORIGINS;
|
|
174
|
+
if (
|
|
175
|
+
!allowed.includes(url.origin) ||
|
|
176
|
+
!/^[A-Za-z0-9_-]{43}$/.test(url.hash.slice(1))
|
|
177
|
+
)
|
|
178
|
+
throw new Error(
|
|
179
|
+
"Invitation is not from the configured Teams Facilitator site.",
|
|
180
|
+
);
|
|
181
|
+
this.origin = url.origin;
|
|
182
|
+
this.stopped = new AbortController();
|
|
183
|
+
// The invitation is single-use, so the connector picks its own credential: a join
|
|
184
|
+
// whose response was lost is repeated and resumes the same connection.
|
|
185
|
+
const result = await this.request("join", {
|
|
186
|
+
invitation: url.hash.slice(1),
|
|
187
|
+
client: this.clientName,
|
|
188
|
+
credential: randomBytes(32).toString("base64url"),
|
|
189
|
+
});
|
|
190
|
+
// "connecting": the meeting worker is still starting; listening waits for it.
|
|
191
|
+
if (
|
|
192
|
+
(result.status !== "joined" && result.status !== "connecting") ||
|
|
193
|
+
typeof result.credential !== "string"
|
|
194
|
+
)
|
|
195
|
+
return result.status === "disconnected"
|
|
196
|
+
? this.stop()
|
|
197
|
+
: { ...DISCONNECTED };
|
|
198
|
+
this.credential = result.credential;
|
|
199
|
+
this.agentName = result.agentName;
|
|
200
|
+
this.mode = result.mode || "text";
|
|
201
|
+
const framework =
|
|
202
|
+
result.frameworkName || FRAMEWORKS[result.framework] || result.framework;
|
|
203
|
+
const note = framework
|
|
204
|
+
? ` The meeting is using the ${framework} framework; if helpful, do a quick search for ${framework} best practices.`
|
|
205
|
+
: "";
|
|
206
|
+
return {
|
|
207
|
+
status: "joined",
|
|
208
|
+
agentName: this.agentName,
|
|
209
|
+
mode: this.mode,
|
|
210
|
+
...(framework ? { framework } : {}),
|
|
211
|
+
instruction: `Call methodiq_wait_for_question.${note} Stop if disconnected; do not automatically rejoin.`,
|
|
212
|
+
};
|
|
158
213
|
}
|
|
159
214
|
async wait({ timeout_seconds = 180 } = {}) {
|
|
160
|
-
const deadline =
|
|
215
|
+
const deadline =
|
|
216
|
+
Date.now() + Math.max(5, Math.min(300, timeout_seconds)) * 1000;
|
|
161
217
|
while (this.credential && Date.now() < deadline) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
218
|
+
const result = await this.request(
|
|
219
|
+
"listen",
|
|
220
|
+
{
|
|
221
|
+
wait: Math.max(
|
|
222
|
+
5,
|
|
223
|
+
Math.min(45, Math.ceil((deadline - Date.now()) / 1000)),
|
|
224
|
+
),
|
|
225
|
+
},
|
|
226
|
+
{ until: deadline },
|
|
227
|
+
);
|
|
228
|
+
if (result.status === "disconnected") return { ...DISCONNECTED };
|
|
229
|
+
if (result.status === "unavailable")
|
|
230
|
+
return {
|
|
231
|
+
status: "reconnecting",
|
|
232
|
+
instruction:
|
|
233
|
+
"Teams Facilitator is briefly unreachable, but your meeting connection is kept. Call methodiq_wait_for_question again.",
|
|
234
|
+
};
|
|
235
|
+
if (result.mode) this.mode = result.mode;
|
|
236
|
+
if (result.status === "question") {
|
|
237
|
+
this.lastQuestionId = result.questionId;
|
|
238
|
+
return {
|
|
239
|
+
status: "question_received",
|
|
240
|
+
id: result.questionId,
|
|
241
|
+
question: result.question,
|
|
242
|
+
speaker: result.speaker,
|
|
243
|
+
isOwner: result.isOwner,
|
|
244
|
+
mode: this.mode,
|
|
245
|
+
recentConversation: result.recentConversation,
|
|
246
|
+
instruction: `${result.instruction} Reply with methodiq_speak_to_room.`,
|
|
247
|
+
};
|
|
171
248
|
}
|
|
172
249
|
}
|
|
173
|
-
return this.credential
|
|
250
|
+
return this.credential
|
|
251
|
+
? {
|
|
252
|
+
status: "idle",
|
|
253
|
+
instruction:
|
|
254
|
+
"Continue with methodiq_wait_for_question; stop if disconnected.",
|
|
255
|
+
}
|
|
256
|
+
: { ...DISCONNECTED };
|
|
174
257
|
}
|
|
175
|
-
|
|
258
|
+
// The default reply ID comes from the question and message, so repeating an answer after
|
|
259
|
+
// a timeout or outage can never make it speak twice.
|
|
260
|
+
async speak({
|
|
261
|
+
message,
|
|
262
|
+
questionId = this.lastQuestionId,
|
|
263
|
+
replyId = replyIdFor(questionId, message),
|
|
264
|
+
}) {
|
|
176
265
|
if (!this.credential) return { ...DISCONNECTED };
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
if (this.realtimeClient) await this.waitForActivity(Math.min(this.heartbeatMs + 5000, deadline - Date.now()));
|
|
189
|
-
else {
|
|
190
|
-
await this.poll();
|
|
191
|
-
try { await delay(this.pollMs, this.stopped.signal); } catch { break; }
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return this.credential ? { status: "pending", replyId, instruction: "Reply is queued; do not submit it again with a different reply ID." } : { ...DISCONNECTED };
|
|
266
|
+
const result = await this.request("say", { message, replyId, questionId });
|
|
267
|
+
if (result.status === "disconnected") return result;
|
|
268
|
+
if (result.status === "failed") return { status: "failed", replyId };
|
|
269
|
+
return {
|
|
270
|
+
status: "sent",
|
|
271
|
+
replyId,
|
|
272
|
+
mode: this.mode,
|
|
273
|
+
spokenToMeeting: this.mode === "voice",
|
|
274
|
+
instruction: "Continue listening with methodiq_wait_for_question.",
|
|
275
|
+
};
|
|
195
276
|
}
|
|
196
277
|
async transcript(args = {}) {
|
|
197
|
-
const result = await this.request("transcript", {
|
|
278
|
+
const result = await this.request("transcript", {
|
|
279
|
+
limit: args.limit || 20,
|
|
280
|
+
});
|
|
198
281
|
if (result.status === "disconnected") return result;
|
|
199
|
-
|
|
282
|
+
const turns = Array.isArray(result.turns) ? result.turns : [];
|
|
283
|
+
return {
|
|
284
|
+
count: turns.length,
|
|
285
|
+
transcript: turns.map((t) => `[${t.speaker}]: ${t.content}`).join("\n"),
|
|
286
|
+
};
|
|
200
287
|
}
|
|
201
288
|
async leave() {
|
|
202
|
-
const credential = this.credential;
|
|
289
|
+
const credential = this.credential || this.leaveCredential;
|
|
203
290
|
this.stop();
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
291
|
+
if (!credential) return { ...DISCONNECTED };
|
|
292
|
+
// Stop locally immediately, but keep the removal credential until the
|
|
293
|
+
// server confirms it: a failed request must remain retryable.
|
|
294
|
+
this.leaveCredential = credential;
|
|
295
|
+
try {
|
|
296
|
+
const result = await this.request(
|
|
297
|
+
"leave",
|
|
298
|
+
{},
|
|
299
|
+
{ credential, once: true },
|
|
300
|
+
);
|
|
301
|
+
if (result.status === "disconnected") {
|
|
302
|
+
this.leaveCredential = null;
|
|
303
|
+
return { ...DISCONNECTED };
|
|
304
|
+
}
|
|
305
|
+
} catch {
|
|
306
|
+
/* Report unconfirmed removal below, preserving the credential. */
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
status: "error",
|
|
310
|
+
message:
|
|
311
|
+
"Local listening and responses have stopped, but Teams Facilitator could not confirm removal from the meeting.",
|
|
312
|
+
instruction:
|
|
313
|
+
"Call methodiq_leave_session again to retry removal. Do not rejoin or respond.",
|
|
314
|
+
};
|
|
207
315
|
}
|
|
208
316
|
}
|
|
209
317
|
|
|
210
318
|
export const tools = [
|
|
211
|
-
{
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
319
|
+
{
|
|
320
|
+
name: "methodiq_join_session",
|
|
321
|
+
description:
|
|
322
|
+
"Join with the unchanged invitation copied from Teams Facilitator.",
|
|
323
|
+
inputSchema: {
|
|
324
|
+
type: "object",
|
|
325
|
+
properties: { invitation: { type: "string" } },
|
|
326
|
+
required: ["invitation"],
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: "methodiq_wait_for_question",
|
|
331
|
+
description: "Wait for a question addressed to you. Stop on disconnected.",
|
|
332
|
+
inputSchema: {
|
|
333
|
+
type: "object",
|
|
334
|
+
properties: { timeout_seconds: { type: "number" } },
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: "methodiq_speak_to_room",
|
|
339
|
+
description: "Respond in the meeting's current voice or text mode.",
|
|
340
|
+
inputSchema: {
|
|
341
|
+
type: "object",
|
|
342
|
+
properties: {
|
|
343
|
+
message: { type: "string" },
|
|
344
|
+
replyId: { type: "string" },
|
|
345
|
+
questionId: { type: "string" },
|
|
346
|
+
},
|
|
347
|
+
required: ["message"],
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "methodiq_get_transcript",
|
|
352
|
+
description: "Get recent meeting context.",
|
|
353
|
+
inputSchema: { type: "object", properties: { limit: { type: "number" } } },
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
name: "methodiq_leave_session",
|
|
357
|
+
description: "Leave and stop responding to the meeting.",
|
|
358
|
+
inputSchema: { type: "object", properties: {} },
|
|
359
|
+
},
|
|
216
360
|
];
|
|
217
|
-
export function setup({
|
|
361
|
+
export function setup({
|
|
362
|
+
local = false,
|
|
363
|
+
serverUrl = process.env.METHODIQ_URL,
|
|
364
|
+
} = {}) {
|
|
218
365
|
const origin = serverUrl ? new URL(serverUrl).origin : undefined;
|
|
219
|
-
if (origin && !/^https?:\/\//.test(origin))
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
366
|
+
if (origin && !/^https?:\/\//.test(origin))
|
|
367
|
+
throw new Error("Teams Facilitator requires an HTTP or HTTPS site origin.");
|
|
368
|
+
return {
|
|
369
|
+
mcpServers: {
|
|
370
|
+
methodiq: {
|
|
371
|
+
command: local ? process.execPath : "npx",
|
|
372
|
+
args: local
|
|
373
|
+
? [fileURLToPath(import.meta.url)]
|
|
374
|
+
: ["--yes", `@methodiq/mcp@${VERSION}`],
|
|
375
|
+
...(origin ? { env: { METHODIQ_URL: origin } } : {}),
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
};
|
|
225
379
|
}
|
|
226
380
|
export function startStdio(connector = new Connector()) {
|
|
227
|
-
const handlers = {
|
|
228
|
-
|
|
381
|
+
const handlers = {
|
|
382
|
+
methodiq_join_session: "join",
|
|
383
|
+
methodiq_wait_for_question: "wait",
|
|
384
|
+
methodiq_speak_to_room: "speak",
|
|
385
|
+
methodiq_get_transcript: "transcript",
|
|
386
|
+
methodiq_leave_session: "leave",
|
|
387
|
+
};
|
|
388
|
+
const send = (value) =>
|
|
389
|
+
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...value }) + "\n");
|
|
229
390
|
const input = readline.createInterface({ input: process.stdin });
|
|
230
|
-
input.on("line", async line => {
|
|
391
|
+
input.on("line", async (line) => {
|
|
231
392
|
let msg;
|
|
232
|
-
try {
|
|
393
|
+
try {
|
|
394
|
+
msg = JSON.parse(line);
|
|
395
|
+
} catch {
|
|
396
|
+
send({ id: null, error: { code: -32700, message: "Invalid JSON" } });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
233
399
|
if (msg.id === undefined) return;
|
|
234
400
|
try {
|
|
235
401
|
let result;
|
|
236
402
|
if (msg.method === "initialize") {
|
|
237
|
-
connector.clientName =
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
403
|
+
connector.clientName =
|
|
404
|
+
typeof msg.params?.clientInfo?.name === "string"
|
|
405
|
+
? msg.params.clientInfo.name.slice(0, 128)
|
|
406
|
+
: undefined;
|
|
407
|
+
const supported = [
|
|
408
|
+
"2025-11-25",
|
|
409
|
+
"2025-06-18",
|
|
410
|
+
"2025-03-26",
|
|
411
|
+
"2024-11-05",
|
|
412
|
+
];
|
|
413
|
+
result = {
|
|
414
|
+
protocolVersion: supported.includes(msg.params?.protocolVersion)
|
|
415
|
+
? msg.params.protocolVersion
|
|
416
|
+
: supported[0],
|
|
417
|
+
capabilities: { tools: {} },
|
|
418
|
+
serverInfo: { name: "@methodiq/mcp", version: VERSION },
|
|
419
|
+
instructions: INSTRUCTIONS,
|
|
420
|
+
};
|
|
421
|
+
} else if (msg.method === "ping") result = {};
|
|
242
422
|
else if (msg.method === "tools/list") result = { tools };
|
|
243
423
|
else if (msg.method === "tools/call") {
|
|
244
424
|
const method = handlers[msg.params?.name];
|
|
245
425
|
if (!method) throw new Error("Unknown tool");
|
|
246
426
|
const value = await connector[method](msg.params.arguments || {});
|
|
247
427
|
result = { content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
248
|
-
} else {
|
|
428
|
+
} else {
|
|
429
|
+
send({
|
|
430
|
+
id: msg.id,
|
|
431
|
+
error: { code: -32601, message: "Unknown method" },
|
|
432
|
+
});
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
249
435
|
send({ id: msg.id, result });
|
|
250
|
-
} catch (error) {
|
|
436
|
+
} catch (error) {
|
|
437
|
+
send({
|
|
438
|
+
id: msg.id,
|
|
439
|
+
result: {
|
|
440
|
+
isError: true,
|
|
441
|
+
content: [{ type: "text", text: error.message }],
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
}
|
|
251
445
|
});
|
|
252
446
|
input.on("close", () => connector.leave().catch(() => connector.stop()));
|
|
253
447
|
return input;
|
|
254
448
|
}
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
449
|
+
if (
|
|
450
|
+
process.argv[1] &&
|
|
451
|
+
fileURLToPath(import.meta.url) === realpathSync(process.argv[1])
|
|
452
|
+
) {
|
|
453
|
+
if (Number(process.versions.node.split(".")[0]) < 22) {
|
|
454
|
+
console.error("Teams Facilitator requires Node.js 22 or newer.");
|
|
455
|
+
process.exitCode = 1;
|
|
456
|
+
} else if (process.argv.includes("--setup")) {
|
|
258
457
|
try {
|
|
259
458
|
const urlIndex = process.argv.indexOf("--url");
|
|
260
|
-
const serverUrl =
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
459
|
+
const serverUrl =
|
|
460
|
+
urlIndex >= 0 ? process.argv[urlIndex + 1] : process.env.METHODIQ_URL;
|
|
461
|
+
if (urlIndex >= 0 && (!serverUrl || serverUrl.startsWith("--")))
|
|
462
|
+
throw new Error("--url requires the Teams Facilitator site's origin.");
|
|
463
|
+
console.log(
|
|
464
|
+
JSON.stringify(
|
|
465
|
+
setup({ local: process.argv.includes("--local"), serverUrl }),
|
|
466
|
+
null,
|
|
467
|
+
2,
|
|
468
|
+
),
|
|
469
|
+
);
|
|
470
|
+
console.error(
|
|
471
|
+
"Replace the existing `methodiq` entry with this standard MCP configuration. Preserve unrelated settings. Reload your tools if supported, or ask the user: 'Please restart this app, then say continue.' Then resume using your invitation. Setup does not edit configuration files.",
|
|
472
|
+
);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
console.error(error.message);
|
|
475
|
+
process.exitCode = 1;
|
|
476
|
+
}
|
|
265
477
|
} else startStdio();
|
|
266
478
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@methodiq/mcp",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Assistant-neutral
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Assistant-neutral Teams Facilitator meeting connector",
|
|
5
|
+
"homepage": "https://www.teamsfacilitator.com/bring-your-own-agent",
|
|
5
6
|
"main": "connector.js",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"bin": {
|
|
@@ -12,9 +13,6 @@
|
|
|
12
13
|
"dev:legacy": "node index.js",
|
|
13
14
|
"test": "node --test connector.test.js"
|
|
14
15
|
},
|
|
15
|
-
"dependencies": {
|
|
16
|
-
"convex": "^1.41.0"
|
|
17
|
-
},
|
|
18
16
|
"engines": {
|
|
19
17
|
"node": ">=22"
|
|
20
18
|
},
|
|
@@ -29,7 +27,8 @@
|
|
|
29
27
|
"keywords": [
|
|
30
28
|
"mcp",
|
|
31
29
|
"model-context-protocol",
|
|
32
|
-
"methodiq"
|
|
30
|
+
"methodiq",
|
|
31
|
+
"teams-facilitator"
|
|
33
32
|
],
|
|
34
33
|
"author": "Methodiq",
|
|
35
34
|
"license": "ISC"
|