Drive Meetily from your own MCP client
Speak JSON-RPC directly to the meetily-pro mcp server over stdio, without an AI client in between.
Most people connect an AI assistant to Meetily with meetily-pro mcp install and never write a line of code against the protocol. This page is for the other case: building your own MCP client that talks to Meetily directly, with no AI assistant in between.
The client speaks JSON-RPC to the meetily-pro mcp server over stdio, the same server your AI assistant would use. The Meetily app must be running, with Integrations and AI Assistants (MCP) enabled in Settings.
The wire protocol
You spawn meetily-pro mcp as a subprocess and keep its stdin and stdout piped. On the same machine the server reads the local loopback token itself; your client never passes or prints a token.
Messages are JSON-RPC 2.0, one JSON object per line, newline-delimited. You write requests to the server's stdin; responses arrive the same way, one JSON object per line on stdout.
The handshake runs in this exact order:
- Send an
initializerequest and read its response. - Send an
initializednotification. A notification has noidand gets no response. - Send a
tools/listrequest and readresult.tools. Each entry has aname. - Call a tool with a
tools/callrequest.
The four lines, in order:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"my-client","version":"0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whoami","arguments":{}}}Note the second line has no id. Sending an id on it, or waiting for a reply to it, will hang your client.
Once the handshake is done, calling further tools is the same tools/call shape with a different name and arguments. The scripts below call whoami (no args), then list_meetings (no args), then get_summary with a meeting_id argument.
Two things to keep in mind about what tools/list returns: the two destructive delete tools are never advertised, in any mode, and the five webhook tools are hidden by default. Start the server with --allow-webhooks if you need those.
Minimal client
Each script does the same thing: spawn the server, run the handshake, call whoami, then list_meetings, then get_summary for a meeting ID you fill in.
Standard library only, no pip installs required.
import json
import subprocess
proc = subprocess.Popen(
["meetily-pro", "mcp"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True,
bufsize=1,
)
def send(msg):
proc.stdin.write(json.dumps(msg) + "\n")
proc.stdin.flush()
def recv():
line = proc.stdout.readline()
return json.loads(line)
send({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "my-client", "version": "0"},
},
})
print(recv())
send({"jsonrpc": "2.0", "method": "notifications/initialized"})
send({"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
tools = recv()["result"]["tools"]
print([t["name"] for t in tools])
send({"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "whoami", "arguments": {}}})
print(recv())
send({"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "list_meetings", "arguments": {}}})
print(recv())
send({
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {"name": "get_summary", "arguments": {"meeting_id": "MEETING_ID"}},
})
print(recv())Run it with python client.py.
Uses the built-in child_process and readline, no dependencies.
const { spawn } = require("child_process");
const readline = require("readline");
const proc = spawn("meetily-pro", ["mcp"], { stdio: ["pipe", "pipe", "inherit"] });
const rl = readline.createInterface({ input: proc.stdout });
const pending = [];
rl.on("line", (line) => {
const resolve = pending.shift();
if (resolve) resolve(JSON.parse(line));
});
function call(msg) {
return new Promise((resolve) => {
pending.push(resolve);
proc.stdin.write(JSON.stringify(msg) + "\n");
});
}
function notify(msg) {
proc.stdin.write(JSON.stringify(msg) + "\n");
}
async function main() {
const init = await call({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: { name: "my-client", version: "0" },
},
});
console.log(init);
notify({ jsonrpc: "2.0", method: "notifications/initialized" });
const list = await call({ jsonrpc: "2.0", id: 2, method: "tools/list" });
console.log(list.result.tools.map((t) => t.name));
const who = await call({
jsonrpc: "2.0",
id: 3,
method: "tools/call",
params: { name: "whoami", arguments: {} },
});
console.log(who);
const meetings = await call({
jsonrpc: "2.0",
id: 4,
method: "tools/call",
params: { name: "list_meetings", arguments: {} },
});
console.log(meetings);
const summary = await call({
jsonrpc: "2.0",
id: 5,
method: "tools/call",
params: { name: "get_summary", arguments: { meeting_id: "MEETING_ID" } },
});
console.log(summary);
proc.stdin.end();
proc.kill();
}
main();Run it with node client.js.
Uses serde_json for parsing, standard library for the process and I/O.
[dependencies]
serde_json = "1"use std::io::{BufRead, BufReader, Lines, Write};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
fn send(stdin: &mut ChildStdin, msg: serde_json::Value) {
writeln!(stdin, "{msg}").expect("write failed");
}
fn recv(lines: &mut Lines<BufReader<ChildStdout>>) -> serde_json::Value {
let line = lines.next().expect("no response").expect("read failed");
serde_json::from_str(&line).expect("bad json")
}
fn main() {
let mut child: Child = Command::new("meetily-pro")
.arg("mcp")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("failed to start meetily-pro mcp");
let mut stdin = child.stdin.take().expect("no stdin");
let stdout = child.stdout.take().expect("no stdout");
let mut lines = BufReader::new(stdout).lines();
send(&mut stdin, serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "my-client", "version": "0" }
}
}));
println!("{:?}", recv(&mut lines));
send(&mut stdin, serde_json::json!({
"jsonrpc": "2.0",
"method": "notifications/initialized"
}));
send(&mut stdin, serde_json::json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }));
let tools_resp = recv(&mut lines);
let names: Vec<&str> = tools_resp["result"]["tools"]
.as_array()
.unwrap()
.iter()
.map(|t| t["name"].as_str().unwrap())
.collect();
println!("{names:?}");
send(&mut stdin, serde_json::json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": { "name": "whoami", "arguments": {} }
}));
println!("{:?}", recv(&mut lines));
send(&mut stdin, serde_json::json!({
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": { "name": "list_meetings", "arguments": {} }
}));
println!("{:?}", recv(&mut lines));
send(&mut stdin, serde_json::json!({
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": { "name": "get_summary", "arguments": { "meeting_id": "MEETING_ID" } }
}));
println!("{:?}", recv(&mut lines));
}Run it with cargo run.
For most people the easy path is meetily-pro mcp install plus an AI client like Claude Desktop. Come back to this page only if you're building your own MCP client that needs to speak JSON-RPC directly.
Last updated on
