#
tokens: 5248/50000 7/7 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── Dockerfile
├── index.ts
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Node.js dependencies
node_modules/

# Bun
.bun/

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# OS specific files
.DS_Store
Thumbs.db

# IDE specific files
.idea/
.vscode/
*.swp
*.swo

# Build outputs
dist/
build/

# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Claude ChatGPT MCP Tool

This is a Model Context Protocol (MCP) tool that allows Claude to interact with the ChatGPT desktop app on macOS.

## Features

- Ask ChatGPT questions directly from Claude
- View ChatGPT conversation history
- Continue existing ChatGPT conversations

## Installation

### Prerequisites

- macOS with M1/M2/M3 chip
- [ChatGPT desktop app](https://chatgpt.com/download) installed
- [Bun](https://bun.sh/) installed
- [Claude desktop app](https://claude.ai/desktop) installed

### NPX Installation (Recommended)

You can use NPX to run this tool without cloning the repository:

- **Install and run the package using NPX:**

```bash
npx claude-chatgpt-mcp
```

- **Configure Claude Desktop:**

Edit your `claude_desktop_config.json` file (located at `~/Library/Application Support/Claude/claude_desktop_config.json`) to include this tool:

```json
"chatgpt-mcp": {
  "command": "npx",
  "args": ["claude-chatgpt-mcp"]
}
```

- **Restart the Claude Desktop app**

- **Grant necessary permissions:**
  - Go to System Preferences > Privacy & Security > Privacy
  - Give Terminal (or iTerm) access to Accessibility features
  - You may see permission prompts when the tool is first used

### Manual Installation

1. Clone this repository:

```bash
git clone https://github.com/syedazharmbnr1/claude-chatgpt-mcp.git
cd claude-chatgpt-mcp
```

2. Install dependencies:

```bash
bun install
```

3. Make sure the script is executable:

```bash
chmod +x index.ts
```

4. Update your Claude Desktop configuration:

Edit your `claude_desktop_config.json` file (located at `~/Library/Application Support/Claude/claude_desktop_config.json`) to include this tool:

```json
"chatgpt-mcp": {
  "command": "/Users/YOURUSERNAME/.bun/bin/bun",
  "args": ["run", "/path/to/claude-chatgpt-mcp/index.ts"]
}
```

Make sure to replace `YOURUSERNAME` with your actual macOS username and adjust the path to where you cloned this repository.

5. Restart Claude Desktop app

6. Grant permissions:
   - Go to System Preferences > Privacy & Security > Privacy
   - Give Terminal (or iTerm) access to Accessibility features
   - You may see permission prompts when the tool is first used

## Usage

Once installed, you can use the ChatGPT tool directly from Claude by asking questions like:

- "Can you ask ChatGPT what the capital of France is?"
- "Show me my recent ChatGPT conversations"
- "Ask ChatGPT to explain quantum computing"

## Troubleshooting

If the tool isn't working properly:

1. Make sure ChatGPT app is installed and you're logged in
2. Verify the path to bun in your claude_desktop_config.json is correct
3. Check that you've granted all necessary permissions
4. Try restarting both Claude and ChatGPT apps

## Optimizations

This fork includes several significant improvements to the original implementation:

### Enhanced AppleScript Robustness

#### Conversation Retrieval
- Added multiple UI element targeting approaches to handle ChatGPT UI changes
- Implemented better error detection with specific error messages
- Added fallback mechanisms using accessibility attributes
- Improved timeout handling with appropriate delays

#### Response Handling
- Replaced fixed waiting times with dynamic response detection
- Added intelligent completion detection that recognizes when ChatGPT has finished typing
- Implemented text stability detection (waits until text stops changing)
- Added response extraction logic to isolate just the relevant response text
- Improved error handling with detailed error messages
- Added post-processing to clean up UI elements from responses
- Implemented incomplete response detection to warn about potential cutoffs

These optimizations make the integration more reliable across different scenarios, more resilient to UI changes in the ChatGPT application, and better at handling longer response times without message cutoff issues.

## License

MIT
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
FROM node:18-slim

WORKDIR /app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy source code
COPY . .

# Build TypeScript code
RUN npm run build

# Set executable permissions for the entry point
RUN chmod +x dist/index.js

# Start the MCP server
CMD ["node", "dist/index.js"]

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "dist",
    "declaration": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["index.ts"],
  "exclude": ["node_modules", "dist"]
}

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "claude-chatgpt-mcp",
  "version": "1.0.1",
  "main": "dist/index.js",
  "type": "module",
  "description": "A Claude MCP tool to interact with the ChatGPT desktop app on macOS",
  "author": "Syed Azhar",
  "license": "MIT",
  "bin": {
    "claude-chatgpt-mcp": "dist/index.js"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/syedazharmbnr1/claude-chatgpt-mcp.git"
  },
  "keywords": [
    "mcp",
    "claude",
    "chatgpt",
    "mac"
  ],
  "scripts": {
    "dev": "bun run index.ts",
    "build": "tsc",
    "prepare": "npm run build",
    "start": "node dist/index.js"
  },
  "dependencies": {
    "@jxa/global-type": "^1.3.6",
    "@jxa/run": "^1.3.6",
    "@modelcontextprotocol/sdk": "^1.5.0",
    "run-applescript": "^7.0.0"
  },
  "devDependencies": {
    "@types/bun": "latest",
    "@types/node": "^22.13.4",
    "typescript": "^5.4.2"
  },
  "files": [
    "dist",
    "README.md"
  ],
  "engines": {
    "node": ">=18"
  }
}

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration for claude-chatgpt-mcp
startCommand:
  type: stdio
  configSchema:
    type: object
    properties:
      logLevel:
        type: string
        enum: [error, warn, info, debug]
        description: "Logging level for the ChatGPT MCP tool"
        default: "info"
    additionalProperties: false
  commandFunction: |
    function(config) {
      const env = { 
        NODE_ENV: 'production'
      };
      
      if (config && config.logLevel) {
        env.LOG_LEVEL = config.logLevel;
      }
      
      return {
        command: 'node',
        args: ['dist/index.js'],
        env: env
      };
    }

# Build configuration
build:
  dockerfile: Dockerfile
  dockerBuildPath: .

# Metadata for Smithery.ai
metadata:
  name: "Claude ChatGPT MCP Tool"
  description: "A Model Context Protocol (MCP) tool that allows Claude to interact with the ChatGPT desktop app on macOS"
  version: "1.0.1"
  author: "Syed Azhar"
  license: "MIT"
  repository: "https://github.com/syedazharmbnr1/claude-chatgpt-mcp"
  keywords: ["mcp", "claude", "chatgpt", "mac"]

```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
	CallToolRequestSchema,
	ListToolsRequestSchema,
	type Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { runAppleScript } from "run-applescript";
import { run } from "@jxa/run";

// Define the ChatGPT tool
const CHATGPT_TOOL: Tool = {
	name: "chatgpt",
	description: "Interact with the ChatGPT desktop app on macOS",
	inputSchema: {
		type: "object",
		properties: {
			operation: {
				type: "string",
				description: "Operation to perform: 'ask' or 'get_conversations'",
				enum: ["ask", "get_conversations"],
			},
			prompt: {
				type: "string",
				description:
					"The prompt to send to ChatGPT (required for ask operation)",
			},
			conversation_id: {
				type: "string",
				description:
					"Optional conversation ID to continue a specific conversation",
			},
		},
		required: ["operation"],
	},
};

const server = new Server(
	{
		name: "ChatGPT MCP Tool",
		version: "1.0.0",
	},
	{
		capabilities: {
			tools: {},
		},
	},
);

// Check if ChatGPT app is installed and running
async function checkChatGPTAccess(): Promise<boolean> {
	try {
		const isRunning = await runAppleScript(`
      tell application "System Events"
        return application process "ChatGPT" exists
      end tell
    `);

		if (isRunning !== "true") {
			console.log("ChatGPT app is not running, attempting to launch...");
			try {
				await runAppleScript(`
          tell application "ChatGPT" to activate
          delay 2
        `);
			} catch (activateError) {
				console.error("Error activating ChatGPT app:", activateError);
				throw new Error(
					"Could not activate ChatGPT app. Please start it manually.",
				);
			}
		}

		return true;
	} catch (error) {
		console.error("ChatGPT access check failed:", error);
		throw new Error(
			`Cannot access ChatGPT app. Please make sure ChatGPT is installed and properly configured. Error: ${error instanceof Error ? error.message : String(error)}`,
		);
	}
}

// Function to send a prompt to ChatGPT
async function askChatGPT(
	prompt: string,
	conversationId?: string,
): Promise<string> {
	await checkChatGPTAccess();
	try {
		// Function to properly encode text for AppleScript, including handling of Chinese characters
		const encodeForAppleScript = (text: string): string => {
			// Only escape double quotes, leave other characters as is
			return text.replace(/"/g, '\\"');
		};

		const encodedPrompt = encodeForAppleScript(prompt);
		
		// Save original clipboard content
		const saveClipboardScript = `
			set savedClipboard to the clipboard
			return savedClipboard
		`;
		const originalClipboard = await runAppleScript(saveClipboardScript);
		const encodedOriginalClipboard = encodeForAppleScript(originalClipboard);
		
		const script = `
      tell application "ChatGPT"
        activate
        delay 1
        tell application "System Events"
          tell process "ChatGPT"
            ${
							conversationId
								? `
              try
                click button "${conversationId}" of group 1 of group 1 of window 1
                delay 1
              end try
            `
								: ""
						}
            -- Clear any existing text in the input field
            keystroke "a" using {command down}
            keystroke (ASCII character 8) -- Delete key
            delay 0.5
            
            -- Set the clipboard to the prompt text
            set the clipboard to "${encodedPrompt}"
            
            -- Paste the prompt and send it
            keystroke "v" using {command down}
            delay 0.5
            keystroke return
            
            -- Wait for the response with dynamic detection
            set maxWaitTime to 120 -- Maximum wait time in seconds
            set waitInterval to 1 -- Check interval in seconds
            set totalWaitTime to 0
            set previousText to ""
            set stableCount to 0
            set requiredStableChecks to 3 -- Number of consecutive stable checks required
            
            repeat while totalWaitTime < maxWaitTime
              delay waitInterval
              set totalWaitTime to totalWaitTime + waitInterval
              
              -- Get current text
              set frontWin to front window
              set allUIElements to entire contents of frontWin
              set conversationText to {}
              repeat with e in allUIElements
                try
                  if (role of e) is "AXStaticText" then
                    set end of conversationText to (description of e)
                  end if
                end try
              end repeat
              
              set AppleScript's text item delimiters to linefeed
              set currentText to conversationText as text
              
              -- Check if text has stabilized (not changing anymore)
              if currentText is equal to previousText then
                set stableCount to stableCount + 1
                if stableCount ≥ requiredStableChecks then
                  -- Text has been stable for multiple checks, assume response is complete
                  exit repeat
                end if
              else
                -- Text changed, reset stable count
                set stableCount to 0
                set previousText to currentText
              end if
              
              -- Check for response completion indicators
              if currentText contains "▍" then
                -- ChatGPT is still typing (blinking cursor indicator)
                set stableCount to 0
              else if currentText contains "Regenerate" or currentText contains "Continue generating" then
                -- Response likely complete if these UI elements are visible
                set stableCount to stableCount + 1
              end if
            end repeat
            
            -- Final check for text content
            if (count of conversationText) = 0 then
              return "No response text found. ChatGPT may still be processing or encountered an error."
            else
              -- Extract just the latest response
              set responseText to ""
              try
                -- Attempt to find the latest response by looking for patterns
                set AppleScript's text item delimiters to linefeed
                set fullText to conversationText as text
                
                -- Look for the prompt in the text to find where the response starts
                set promptPattern to "${prompt.replace(/"/g, '\\"').replace(/\n/g, ' ')}"
                if fullText contains promptPattern then
                  set promptPos to offset of promptPattern in fullText
                  if promptPos > 0 then
                    -- Get text after the prompt
                    set responseText to text from (promptPos + (length of promptPattern)) to end of fullText
                  end if
                end if
                
                -- If we couldn't find the prompt, return the full text
                if responseText is "" then
                  set responseText to fullText
                end if
                
                return responseText
              on error
                -- Fallback to returning all text if parsing fails
                return conversationText as text
              end try
            end if
          end tell
        end tell
      end tell
    `;
		const result = await runAppleScript(script);
		
		// Restore original clipboard content
		await runAppleScript(`set the clipboard to "${encodedOriginalClipboard}"`);
		
		// Post-process the result to clean up any UI text that might have been captured
		let cleanedResult = result
			.replace(/Regenerate( response)?/g, '')
			.replace(/Continue generating/g, '')
			.replace(/▍/g, '')
			.trim();
			
		// More context-aware incomplete response detection
		const isLikelyComplete = 
			cleanedResult.length > 50 || // Longer responses are likely complete
			cleanedResult.endsWith('.') || 
			cleanedResult.endsWith('!') || 
			cleanedResult.endsWith('?') ||
			cleanedResult.endsWith(':') ||
			cleanedResult.endsWith(')') ||
			cleanedResult.endsWith('}') ||
			cleanedResult.endsWith(']') ||
			cleanedResult.includes('\n\n') || // Multiple paragraphs suggest completeness
			/^[A-Z].*[.!?]$/.test(cleanedResult); // Complete sentence structure
			
		if (cleanedResult.length > 0 && !isLikelyComplete) {
			console.warn("Warning: ChatGPT response may be incomplete");
		}
		
		return cleanedResult;
	} catch (error) {
		console.error("Error interacting with ChatGPT:", error);
		throw new Error(
			`Failed to get response from ChatGPT: ${
				error instanceof Error ? error.message : String(error)
			}`,
		);
	}
}

// Function to get available conversations
async function getConversations(): Promise<string[]> {
	try {
		// Run AppleScript to get conversations from ChatGPT app
		const result = await runAppleScript(`
      -- Check if ChatGPT is running
      tell application "System Events"
        if not (application process "ChatGPT" exists) then
          return "ChatGPT is not running"
        end if
      end tell

      tell application "ChatGPT"
        -- Activate ChatGPT and give it time to respond
        activate
        delay 1.5

        tell application "System Events"
          tell process "ChatGPT"
            -- Check if ChatGPT window exists
            if not (exists window 1) then
              return "No ChatGPT window found"
            end if
            
            -- Try to get conversation titles with multiple approaches
            set conversationsList to {}
            
            try
              -- First attempt: try buttons in group 1 of group 1
              if exists group 1 of group 1 of window 1 then
                set chatButtons to buttons of group 1 of group 1 of window 1
                repeat with chatButton in chatButtons
                  set buttonName to name of chatButton
                  if buttonName is not "New chat" then
                    set end of conversationsList to buttonName
                  end if
                end repeat
              end if
              
              -- If we didn't find any conversations, try an alternative approach
              if (count of conversationsList) is 0 then
                -- Try to find UI elements by accessibility description
                set uiElements to UI elements of window 1
                repeat with elem in uiElements
                  try
                    if exists (attribute "AXDescription" of elem) then
                      set elemDesc to value of attribute "AXDescription" of elem
                      if elemDesc is not "New chat" and elemDesc is not "" then
                        set end of conversationsList to elemDesc
                      end if
                    end if
                  end try
                end repeat
              end if
              
              -- If still no conversations found, return a specific message
              if (count of conversationsList) is 0 then
                return "No conversations found"
              end if
            on error errMsg
              -- Return error message for debugging
              return "Error: " & errMsg
            end try
            
            return conversationsList
          end tell
        end tell
      end tell
    `);

		// Parse the AppleScript result into an array
		if (result === "ChatGPT is not running") {
			console.error("ChatGPT application is not running");
			throw new Error("ChatGPT application is not running");
		} else if (result === "No ChatGPT window found") {
			console.error("No ChatGPT window found");
			throw new Error("No ChatGPT window found");
		} else if (result === "No conversations found") {
			console.error("No conversations found in ChatGPT");
			return []; // Return empty array instead of error message
		} else if (result.startsWith("Error:")) {
			console.error(result);
			throw new Error(result);
		}
		
		const conversations = result.split(", ");
		return conversations;
	} catch (error) {
		console.error("Error getting ChatGPT conversations:", error);
		throw new Error("Error retrieving conversations: " + (error instanceof Error ? error.message : String(error)));
	}
}

function isChatGPTArgs(args: unknown): args is {
	operation: "ask" | "get_conversations";
	prompt?: string;
	conversation_id?: string;
} {
	if (typeof args !== "object" || args === null) return false;

	const { operation, prompt, conversation_id } = args as any;

	if (!operation || !["ask", "get_conversations"].includes(operation)) {
		return false;
	}

	// Validate required fields based on operation
	if (operation === "ask" && !prompt) return false;

	// Validate field types if present
	if (prompt && typeof prompt !== "string") return false;
	if (conversation_id && typeof conversation_id !== "string") return false;

	return true;
}

server.setRequestHandler(ListToolsRequestSchema, async () => ({
	tools: [CHATGPT_TOOL],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
	try {
		const { name, arguments: args } = request.params;

		if (!args) {
			throw new Error("No arguments provided");
		}

		if (name === "chatgpt") {
			if (!isChatGPTArgs(args)) {
				throw new Error("Invalid arguments for ChatGPT tool");
			}

			switch (args.operation) {
				case "ask": {
					if (!args.prompt) {
						throw new Error("Prompt is required for ask operation");
					}

					const response = await askChatGPT(args.prompt, args.conversation_id);

					return {
						content: [
							{
								type: "text",
								text: response || "No response received from ChatGPT.",
							},
						],
						isError: false,
					};
				}

				case "get_conversations": {
					const conversations = await getConversations();

					return {
						content: [
							{
								type: "text",
								text:
									conversations.length > 0
										? `Found ${conversations.length} conversation(s):\n\n${conversations.join("\n")}`
										: "No conversations found in ChatGPT.",
							},
						],
						isError: false,
					};
				}

				default:
					throw new Error(`Unknown operation: ${args.operation}`);
			}
		}

		return {
			content: [{ type: "text", text: `Unknown tool: ${name}` }],
			isError: true,
		};
	} catch (error) {
		return {
			content: [
				{
					type: "text",
					text: `Error: ${error instanceof Error ? error.message : String(error)}`,
				},
			],
			isError: true,
		};
	}
});

const transport = new StdioServerTransport();

await server.connect(transport);
console.error("ChatGPT MCP Server running on stdio");

```