diff --git a/Tools/TranscriptedMCP/README.md b/Tools/TranscriptedMCP/README.md index c94d0eaa..c0bc785c 100644 --- a/Tools/TranscriptedMCP/README.md +++ b/Tools/TranscriptedMCP/README.md @@ -13,6 +13,18 @@ For Claude Desktop users, the best setup is inside the app: That path installs the bundled helper, updates Claude Desktop's config, and runs a local self-test. +## Interactive UI (MCP Apps) + +The server exposes one interactive widget via the MCP Apps extension +(`io.modelcontextprotocol/ui`, SEP-1865): call `show_recent_meetings` to render a +card list of recent meetings — each with inline audio playback and a raw-transcript +view — that draws inside a rendering-capable client. It is fully self-contained +(no network; local audio/transcripts travel over the local transport), and falls +back to a plain-text list in clients that do not render inline UI (e.g. the Claude +Code CLI). Renders inline today in ChatGPT, VS Code, Cursor, and Goose; see +[`docs/mcp-ui-recent-meetings.md`](../../docs/mcp-ui-recent-meetings.md) for the +per-client support matrix and how to try it. + ## Source Build Use this only when developing the MCP server directly: diff --git a/Tools/TranscriptedMCP/Sources/TranscriptedMCP/Main.swift b/Tools/TranscriptedMCP/Sources/TranscriptedMCP/Main.swift index c9c88f34..2c84a35a 100644 --- a/Tools/TranscriptedMCP/Sources/TranscriptedMCP/Main.swift +++ b/Tools/TranscriptedMCP/Sources/TranscriptedMCP/Main.swift @@ -69,10 +69,16 @@ struct TranscriptedMCP { let server = Server( name: "transcripted", version: serverVersion, - capabilities: .init(tools: .init(listChanged: false)) + capabilities: .init( + resources: .init(subscribe: false, listChanged: false), + tools: .init(listChanged: false) + ) ) await registerToolHandlers(server: server, index: index, directories: directories) + await TranscriptedUIResources.register( + server: server, index: index, directories: directories, serverVersion: serverVersion + ) log("MCP server ready, waiting for connections") diff --git a/Tools/TranscriptedMCP/Sources/TranscriptedMCP/RecentMeetingsWidget.swift b/Tools/TranscriptedMCP/Sources/TranscriptedMCP/RecentMeetingsWidget.swift new file mode 100644 index 00000000..19b06b59 --- /dev/null +++ b/Tools/TranscriptedMCP/Sources/TranscriptedMCP/RecentMeetingsWidget.swift @@ -0,0 +1,361 @@ +import Foundation +import TranscriptedCaptureKit + +// MARK: - Widget data model + +/// One meeting rendered as a card in the recent-meetings widget. This is the +/// contract between the data builder (which reads the local capture library) +/// and the self-contained HTML renderer. It is also the `structuredContent` +/// shape a spec-compliant MCP Apps host receives via `ui/notifications/tool-result`, +/// so a template that reads `window.openai.toolOutput` renders the same view as +/// the data baked into the HTML. +struct WidgetMeeting: Codable, Hashable { + /// Human title (frontmatter title, else a filename-derived label). + let title: String + /// `YYYY-MM-DD` local date. + let date: String + /// ISO-8601 datetime when known (used only for display/sort stability). + let datetime: String + /// Recording length in seconds. + let durationSeconds: Int + /// Speaker display names, in transcript order. + let speakers: [String] + /// Total word count across the transcript. + let wordCount: Int + /// The `read_meeting` filename (stem) — lets an agent drill in for more. + let filename: String + /// Full dialogue text (bounded by the same read cap the read tools use). + let transcript: String + /// Inline audio, when a preferred track exists and fits the size budget. + let audio: WidgetAudio? + /// Absolute local path to the audio directory, for the "open in Finder" + /// fallback when audio is absent or too large to inline. Never leaves the Mac. + let audioDirectory: String? + /// Why inline audio is unavailable, when it is (missing vs. over budget). + let audioNote: String? + + enum CodingKeys: String, CodingKey { + case title, date, datetime + case durationSeconds = "duration_seconds" + case speakers + case wordCount = "word_count" + case filename, transcript, audio + case audioDirectory = "audio_directory" + case audioNote = "audio_note" + } +} + +/// A base64 `data:` audio payload the widget plays with a plain `