@rxreyn3/speak-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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryan Reynolds
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,88 @@
1
+ # Speak MCP
2
+
3
+ A local MCP server that rewrites text with Ollama for natural speech and reads it aloud with macOS `say`.
4
+
5
+ ## Requirements
6
+
7
+ - macOS with the `say` command available.
8
+ - Node.js 18 or newer.
9
+ - Ollama running locally.
10
+ - The configured Ollama model pulled locally.
11
+
12
+ ## Installation
13
+
14
+ You do not need to clone this repository to use Speak MCP. Configure your MCP client to run the published npm package with `npx`.
15
+
16
+ ## Claude Desktop configuration
17
+
18
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` and add this server under `mcpServers`:
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "speak": {
24
+ "command": "npx",
25
+ "args": ["-y", "@rxreyn3/speak-mcp"],
26
+ "env": {
27
+ "OLLAMA_HOST": "http://localhost:11434",
28
+ "OLLAMA_MODEL": "qwen2.5:7b-instruct"
29
+ }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ Restart Claude Desktop after changing the config.
36
+
37
+ ## Local development
38
+
39
+ For local development from a clone:
40
+
41
+ ```bash
42
+ npm install
43
+ npm run build
44
+ ```
45
+
46
+ Then point your MCP client at the built entrypoint:
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "speak": {
52
+ "command": "node",
53
+ "args": ["/absolute/path/to/speak-mcp/dist/index.js"],
54
+ "env": {
55
+ "OLLAMA_HOST": "http://localhost:11434",
56
+ "OLLAMA_MODEL": "qwen2.5:7b-instruct"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## Tools
64
+
65
+ ### `speak_text`
66
+
67
+ Reads explicit text aloud. The MCP client must pass the text; this server cannot read the client transcript itself.
68
+
69
+ ```json
70
+ { "text": "Text to read aloud" }
71
+ ```
72
+
73
+ The server rewrites the text with Ollama before playback. If Ollama is unreachable, returns an MCP tool error and does not speak raw text.
74
+
75
+ ### `stop_speech`
76
+
77
+ Stops the current macOS `say` playback, if any. This tool does not contact Ollama and does not start replacement audio.
78
+
79
+ ## Prompt
80
+
81
+ The server registers `read_aloud`, a user-selected prompt that instructs an MCP client to call `speak_text` with visible assistant response text when read-aloud behavior is requested.
82
+
83
+ ## Troubleshooting
84
+
85
+ - If Claude Desktop does not show the server, confirm Node.js 18+ is installed and restart Claude Desktop after editing the config.
86
+ - If speech does not start, confirm Ollama is running at `OLLAMA_HOST` and the model in `OLLAMA_MODEL` is pulled.
87
+ - Do not add `console.log` to the server: stdout is reserved for MCP JSON-RPC messages. Use `console.error` for diagnostics.
88
+ - Late `say` playback failures are logged to stderr and MCP logging; there is intentionally no `speech_status` tool in v1.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { SpeakMcpServer } from './server.js';
3
+ async function main() {
4
+ try {
5
+ const server = new SpeakMcpServer();
6
+ await server.start();
7
+ }
8
+ catch (error) {
9
+ console.error('Failed to start Speak MCP server:', error);
10
+ process.exit(1);
11
+ }
12
+ }
13
+ main().catch((error) => {
14
+ console.error('Unhandled Speak MCP server error:', error);
15
+ process.exit(1);
16
+ });
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,KAAK,UAAU,IAAI;IACjB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,cAAc,EAAE,CAAC;QACpC,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,KAAK,CAAC,CAAC;QAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,KAAK,CAAC,CAAC;IAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ export declare class SpeakMcpServer {
2
+ private readonly server;
3
+ private readonly speech;
4
+ constructor();
5
+ start(): Promise<void>;
6
+ private registerTools;
7
+ private registerPrompts;
8
+ private registerShutdownHandlers;
9
+ }
package/dist/server.js ADDED
@@ -0,0 +1,98 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { rewriteForSpeech, SpeechController } from './speech.js';
5
+ import { textResult } from './types.js';
6
+ export class SpeakMcpServer {
7
+ server;
8
+ speech;
9
+ constructor() {
10
+ this.server = new McpServer({ name: 'speak-mcp', version: '0.1.0' }, { capabilities: { tools: {}, prompts: {}, logging: {} } });
11
+ this.speech = new SpeechController((message) => {
12
+ console.error(message);
13
+ if (this.server.isConnected()) {
14
+ void this.server.sendLoggingMessage({ level: 'error', data: message });
15
+ }
16
+ });
17
+ this.registerTools();
18
+ this.registerPrompts();
19
+ this.registerShutdownHandlers();
20
+ }
21
+ async start() {
22
+ const transport = new StdioServerTransport();
23
+ await this.server.connect(transport);
24
+ console.error('Speak MCP server running on stdio');
25
+ }
26
+ registerTools() {
27
+ this.server.registerTool('speak_text', {
28
+ title: 'Speak text',
29
+ description: 'Rewrite the provided text for natural speech with local Ollama, then read it aloud using macOS say. Pass the exact assistant response or other text to read; the server cannot access the MCP client transcript itself.',
30
+ inputSchema: {
31
+ text: z.string().describe('The assistant response or other text to read aloud.'),
32
+ },
33
+ }, async ({ text }) => {
34
+ const source = text.trim();
35
+ if (!source)
36
+ return textResult('No text was provided to speak.', true);
37
+ this.speech.stopCurrent();
38
+ let rewritten;
39
+ try {
40
+ rewritten = await rewriteForSpeech(source);
41
+ }
42
+ catch (err) {
43
+ const cause = err?.cause?.code;
44
+ const message = cause === 'ECONNREFUSED'
45
+ ? 'Ollama is not reachable at the configured host.'
46
+ : `Speech rewrite failed: ${err.message}`;
47
+ return textResult(message, true);
48
+ }
49
+ this.speech.speak(rewritten);
50
+ return textResult('Speech started.');
51
+ });
52
+ this.server.registerTool('stop_speech', {
53
+ title: 'Stop speech',
54
+ description: 'Stop any speech currently playing from the local macOS say process.',
55
+ inputSchema: {},
56
+ }, async () => textResult(this.speech.stopCurrent() ? 'Speech stopped.' : 'No speech is currently playing.'));
57
+ }
58
+ registerPrompts() {
59
+ this.server.registerPrompt('read_aloud', {
60
+ title: 'Read aloud',
61
+ description: 'Instructions for using speak_text to read assistant responses aloud.',
62
+ }, async () => ({
63
+ description: 'Use the speak_text tool when the user asks to hear text read aloud.',
64
+ messages: [
65
+ {
66
+ role: 'user',
67
+ content: {
68
+ type: 'text',
69
+ text: 'When I ask you to read something aloud, call the speak_text tool with the exact text to speak. For a previous assistant response, pass the visible response text. Use stop_speech when I ask you to stop playback.',
70
+ },
71
+ },
72
+ ],
73
+ }));
74
+ }
75
+ registerShutdownHandlers() {
76
+ const stopPlayback = () => {
77
+ this.speech.stopCurrent();
78
+ };
79
+ const shutdown = async (signal) => {
80
+ stopPlayback();
81
+ try {
82
+ await this.server.close();
83
+ }
84
+ catch (err) {
85
+ console.error(`Failed to close MCP server after ${signal}:`, err);
86
+ }
87
+ process.exit(0);
88
+ };
89
+ process.once('SIGINT', () => {
90
+ void shutdown('SIGINT');
91
+ });
92
+ process.once('SIGTERM', () => {
93
+ void shutdown('SIGTERM');
94
+ });
95
+ process.once('beforeExit', stopPlayback);
96
+ }
97
+ }
98
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { PlaybackState } from './types.js';
2
+ export declare const rewriteForSpeech: (text: string) => Promise<string>;
3
+ type PlaybackLogger = (message: string) => void | Promise<void>;
4
+ export declare class SpeechController {
5
+ private readonly log;
6
+ private current;
7
+ private state;
8
+ constructor(log?: PlaybackLogger);
9
+ getState(): PlaybackState;
10
+ stopCurrent(): boolean;
11
+ speak(rewrittenText: string): void;
12
+ }
13
+ export {};
package/dist/speech.js ADDED
@@ -0,0 +1,95 @@
1
+ import { spawn } from 'node:child_process';
2
+ const DEFAULT_MODEL = 'qwen2.5:7b-instruct';
3
+ const DEFAULT_HOST = 'http://localhost:11434';
4
+ const REWRITE_TIMEOUT_MS = 60_000;
5
+ const SPEECH_SYSTEM_PROMPT = [
6
+ "You rewrite an AI assistant's chat response so it can be read aloud by a text-to-speech engine.",
7
+ 'Narrate the prose naturally, as if explaining it out loud.',
8
+ 'Do NOT read code verbatim. When you encounter a code block, briefly describe in one or two spoken sentences what the code does — never spell out syntax, punctuation, or individual symbols.',
9
+ 'Reduce markdown, URLs, and file paths to their spoken essence.',
10
+ 'Output only the spoken text — no preamble, no headings, no markup.',
11
+ ].join(' ');
12
+ export const rewriteForSpeech = async (text) => {
13
+ const host = (process.env.OLLAMA_HOST?.trim() || DEFAULT_HOST).replace(/\/$/, '');
14
+ const model = process.env.OLLAMA_MODEL?.trim() || DEFAULT_MODEL;
15
+ const controller = new AbortController();
16
+ const timer = setTimeout(() => controller.abort(), REWRITE_TIMEOUT_MS);
17
+ try {
18
+ const res = await fetch(`${host}/api/chat`, {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body: JSON.stringify({
22
+ model,
23
+ stream: false,
24
+ messages: [
25
+ { role: 'system', content: SPEECH_SYSTEM_PROMPT },
26
+ { role: 'user', content: text },
27
+ ],
28
+ options: { temperature: 0, seed: 42, num_predict: 4096 },
29
+ }),
30
+ signal: controller.signal,
31
+ });
32
+ const body = (await res.json().catch(() => null));
33
+ if (!res.ok) {
34
+ const detail = body?.error ? `: ${body.error}` : '';
35
+ throw new Error(`Ollama returned status ${res.status}${detail}`);
36
+ }
37
+ const rewritten = body?.message?.content?.trim();
38
+ if (!rewritten)
39
+ throw new Error('Ollama returned an empty rewrite');
40
+ return rewritten;
41
+ }
42
+ finally {
43
+ clearTimeout(timer);
44
+ }
45
+ };
46
+ export class SpeechController {
47
+ log;
48
+ current = null;
49
+ state = { status: 'idle' };
50
+ constructor(log = (message) => console.error(message)) {
51
+ this.log = log;
52
+ }
53
+ getState() {
54
+ return this.state;
55
+ }
56
+ stopCurrent() {
57
+ if (!this.current)
58
+ return false;
59
+ this.current.kill();
60
+ this.current = null;
61
+ this.state = { status: 'idle', lastError: this.state.lastError };
62
+ return true;
63
+ }
64
+ speak(rewrittenText) {
65
+ this.stopCurrent();
66
+ const child = spawn('say', [], { stdio: ['pipe', 'ignore', 'ignore'] });
67
+ this.current = child;
68
+ this.state = { status: 'playing', startedAt: new Date() };
69
+ child.on('error', (err) => {
70
+ if (this.current !== child)
71
+ return;
72
+ this.current = null;
73
+ const message = `Could not run 'say': ${err.message}`;
74
+ this.state = { status: 'idle', lastError: message };
75
+ void this.log(message);
76
+ });
77
+ child.on('close', (code) => {
78
+ if (this.current === child) {
79
+ this.current = null;
80
+ if (code) {
81
+ const message = `'say' exited with code ${code}`;
82
+ this.state = { status: 'idle', lastError: message };
83
+ void this.log(message);
84
+ return;
85
+ }
86
+ this.state = { status: 'idle' };
87
+ }
88
+ });
89
+ child.stdin?.on('error', () => {
90
+ // Ignore EPIPE if the child was killed mid-write.
91
+ });
92
+ child.stdin?.end(rewrittenText);
93
+ }
94
+ }
95
+ //# sourceMappingURL=speech.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech.js","sourceRoot":"","sources":["../src/speech.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ export type TextContent = {
2
+ type: 'text';
3
+ text: string;
4
+ };
5
+ export type ToolTextResult = {
6
+ content: TextContent[];
7
+ isError?: boolean;
8
+ };
9
+ export type PlaybackState = {
10
+ status: 'idle';
11
+ lastError?: string;
12
+ } | {
13
+ status: 'playing';
14
+ startedAt: Date;
15
+ lastError?: string;
16
+ };
17
+ export declare const textResult: (text: string, isError?: boolean) => ToolTextResult;
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ export const textResult = (text, isError = false) => ({
2
+ content: [{ type: 'text', text }],
3
+ ...(isError ? { isError: true } : {}),
4
+ });
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAcA,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,OAAO,GAAG,KAAK,EAAkB,EAAE,CAAC,CAAC;IAC5E,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACjC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;CACtC,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ {
2
+ "mcpServers": {
3
+ "speak": {
4
+ "command": "npx",
5
+ "args": ["-y", "@rxreyn3/speak-mcp"],
6
+ "env": {
7
+ "OLLAMA_HOST": "http://localhost:11434",
8
+ "OLLAMA_MODEL": "qwen2.5:7b-instruct"
9
+ }
10
+ }
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@rxreyn3/speak-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Local MCP server for Ollama-rewritten macOS speech playback",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "speak-mcp": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "typecheck": "tsc --noEmit",
14
+ "clean": "rm -rf dist",
15
+ "prepublishOnly": "npm run clean && npm run typecheck && npm run build"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "tts",
21
+ "ollama",
22
+ "speech",
23
+ "read-aloud",
24
+ "macos"
25
+ ],
26
+ "author": "Ryan Reynolds",
27
+ "license": "MIT",
28
+ "type": "module",
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "README.md",
35
+ "LICENSE",
36
+ "examples"
37
+ ],
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.12.1",
40
+ "zod": "^3.25.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.15.30",
44
+ "typescript": "^5.8.3"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/rxreyn3/speak-mcp.git"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/rxreyn3/speak-mcp/issues"
52
+ },
53
+ "homepage": "https://github.com/rxreyn3/speak-mcp#readme",
54
+ "publishConfig": {
55
+ "access": "public"
56
+ }
57
+ }