Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Screencast MCP</title>
<meta name="description" content="A Windows-first MCP server for screen recording, frame sampling, and minimal ffmpeg edits.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>&#127916;</text></svg>">
<meta property="og:title" content="Screencast MCP">
<meta property="og:description" content="A Windows-first MCP server for screen recording, frame sampling, and minimal ffmpeg edits.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://tmhsdigital.github.io/screencast-mcp/">
<meta name="twitter:card" content="summary">
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
Expand Down Expand Up @@ -39,13 +45,13 @@ <h1>Screencast MCP</h1>
a small set of ffmpeg edits. It speaks MCP over stdio.</p>
<p><a href="https://github.com/TMHSDigital/screencast-mcp">View on GitHub</a></p>

<h2>Prerequisites</h2>
<h2 id="prerequisites">Prerequisites</h2>
<p><code>ffmpeg</code> and <code>ffprobe</code> must be installed and on
<code>PATH</code> (or set via <code>FFMPEG_PATH</code> /
<code>FFPROBE_PATH</code>). Screen capture uses <code>gdigrab</code>, which is
Windows-only; the watch and edit tools run anywhere ffmpeg runs.</p>

<h2>Install</h2>
<h2 id="install">Install</h2>
<pre><code>npm install -g @tmhs/screencast-mcp</code></pre>
<pre><code>{
"mcpServers": {
Expand All @@ -56,7 +62,10 @@ <h2>Install</h2>
}
}</code></pre>

<h2>Tools</h2>
<h2 id="tools">Tools</h2>
<p>Tools that write a file refuse to replace an existing file at a
caller-supplied <code>output</code> path unless <code>overwrite: true</code>
is passed; auto-generated default paths are always unique.</p>
<table>
<thead><tr><th>Tool</th><th>What it does</th></tr></thead>
<tbody>
Expand Down Expand Up @@ -88,12 +97,14 @@ <h2>Tools</h2>
</tbody>
</table>

<h2>Windows notes</h2>
<h2 id="windows-notes">Windows notes</h2>
<ul>
<li>Monitor targets crop the virtual desktop to a display's real pixel
bounds, so <code>monitor:1</code> grabs the second display at its true
offset; <code>monitor:0</code> is primary.</li>
<li>Window capture matches by exact title.</li>
<li>Window capture matches a case-insensitive exact title first, then
falls back to a substring match; with several matches the topmost window
wins.</li>
<li>Fullscreen-exclusive apps can produce black frames under gdigrab; use
borderless-windowed mode.</li>
<li>System audio capture (<code>start_recording</code> with
Expand All @@ -103,7 +114,7 @@ <h2>Windows notes</h2>
supported.</li>
</ul>

<h2>Threat model</h2>
<h2 id="threat-model">Threat model</h2>
<div class="note">
Screen capture can record anything on screen, including secrets. Capture is
always explicit (a tool call, never automatic), output stays on the local
Expand Down
37 changes: 37 additions & 0 deletions src/__tests__/docs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Drift guard: the tools table on the GitHub Pages site (docs/index.html) is
* hand-mirrored from mcp-tools.json (the registered-tool manifest). Nothing
* else keeps them in sync, so this test fails a PR that adds/removes a tool
* without updating the site, or documents a tool that does not exist.
*/
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..");

const registered = (
JSON.parse(readFileSync(join(root, "mcp-tools.json"), "utf8")) as Array<{
name: string;
}>
).map((t) => t.name);

const html = readFileSync(join(root, "docs", "index.html"), "utf8");
const documented = [...html.matchAll(/<td><code>([a-z_]+)<\/code><\/td>/g)].map(
(m) => m[1],
);

describe("docs/index.html tools table stays in sync with mcp-tools.json", () => {
it("documents every registered tool", () => {
const missing = registered.filter((t) => !documented.includes(t));
expect(missing, `tools missing from docs/index.html: ${missing.join(", ")}`).toEqual([]);
});
it("documents no tool that is not registered", () => {
const phantom = documented.filter((t) => !registered.includes(t));
expect(phantom, `tools in docs/index.html but not registered: ${phantom.join(", ")}`).toEqual([]);
});
it("actually parsed the table (sanity check against a silent regex miss)", () => {
expect(documented.length).toBeGreaterThan(0);
});
});
Loading