# Directory Structure
```
├── .github
│ └── workflows
│ └── python-publish.yml
├── .gitignore
├── LICENSE
├── pyproject.toml
├── README.md
├── src
│ └── mcp_server_chatgpt
│ ├── __init__.py
│ └── server.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# mcp-server-chatgpt-app
## Prerequisite
- [ChatGPT macOS app](https://openai.com/chatgpt/download/)
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
- Install the ["Ask ChatGPT on Mac" shortcuts](https://www.icloud.com/shortcuts/6ae86a39a31e4ec5938abad953ecfd64)
## Usage
### cursor
update `.mcp.json` to add the following:
```
{
"mcpServers": {
"chatgpt": {
"command": "uvx",
"args": ["mcp-server-chatgpt-app"],
"env": {},
"disabled": false,
"autoApprove": []
}
}
}
```
### chatwise
Go to Settings -> Tools -> Add and use the following config:
```
Type: stdio
ID: ChatGPT
Command: uvx mcp-server-chatgpt-app
```
> [!CAUTION]
> Chatwise did not close mcp server even when itself is closed, which may lead to multiple mcp servers running at the same time.
> DO remember to clean them all up: `pkill -f 'mcp-server-chatgpt-app'`
### [MCP Inspector](https://github.com/modelcontextprotocol/inspector)
```
Transport type: stdio
Command: uv
Arguments: --directory /Users/<your-username>/Developer/mcp-server-chatgpt-app/src/mcp_server_chatgpt run server.py
Configuration/Request Timeout: 100000
```
## local development
```
uv --directory $HOME/Developer/mcp-server-chatgpt-app/src/mcp_server_chatgpt run server.py
```
```
--------------------------------------------------------------------------------
/src/mcp_server_chatgpt/__init__.py:
--------------------------------------------------------------------------------
```python
from .server import main
__all__ = ['main']
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[project]
name = "mcp-server-chatgpt-app"
version = "0.1.5"
description = "MCP Server for ChatGPT via AppleScript"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [
{name = "cdpath", email = "[email protected]"}
]
keywords = ["ChatGPT", "mcp", "AppleScript"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
]
dependencies = [
"mcp>=1.6.0",
]
[project.urls]
Homepage = "https://github.com/cdpath/mcp-server-chatgpt-app"
Repository = "https://github.com/cdpath/mcp-server-chatgpt-app.git"
Issues = "https://github.com/cdpath/mcp-server-chatgpt-app/issues"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/mcp_server_chatgpt"]
[project.scripts]
mcp-server-chatgpt-app = "mcp_server_chatgpt.server:main"
```
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
```yaml
# This workflow will upload a Python Package to PyPI when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Upload Python Package
on:
release:
types: [published]
permissions:
contents: read
jobs:
release-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Build release distributions
run: |
# NOTE: put your own distribution build steps here.
python -m pip install build
python -m build
- name: Upload distributions
uses: actions/upload-artifact@v4
with:
name: release-dists
path: dist/
pypi-publish:
runs-on: ubuntu-latest
needs:
- release-build
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
# Dedicated environments with protections for publishing are strongly recommended.
# For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
environment:
name: pypi
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
# url: https://pypi.org/p/YOURPROJECT
#
# ALTERNATIVE: if your GitHub Release name is the PyPI project version string
# ALTERNATIVE: exactly, uncomment the following line instead:
# url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
steps:
- name: Retrieve release distributions
uses: actions/download-artifact@v4
with:
name: release-dists
path: dist/
- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
```
--------------------------------------------------------------------------------
/src/mcp_server_chatgpt/server.py:
--------------------------------------------------------------------------------
```python
import subprocess
import os
from typing import Optional, Dict, Any
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("ChatGPT")
def run_applescript(script: str, wait_for_output: bool = True) -> Dict[str, Any]:
"""
Run an AppleScript and return its output and status.
Args:
script: The AppleScript to run
wait_for_output: Whether to wait for and capture output
"""
try:
if wait_for_output:
# Run synchronously and capture output
result = subprocess.run(
['osascript', '-e', script],
capture_output=True,
text=True,
check=True
)
return {
"success": True,
"output": result.stdout.strip()
}
else:
# Use Popen for non-blocking execution
subprocess.Popen(
['osascript', '-e', script],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
return {
"success": True,
"output": ""
}
except Exception as e:
error_msg = str(e)
return {
"success": False,
"error": error_msg,
}
@mcp.tool()
def ask_chatgpt(prompt: str, wait_for_output: bool = False) -> Dict[str, Any]:
"""
Send a prompt to ChatGPT macOS app using Shortcuts.
Args:
prompt: The text to send to ChatGPT
wait_for_output: Whether to wait for ChatGPT to respond
Returns:
Dict containing operation status and response
"""
try:
if wait_for_output:
# Use shortcuts command directly to capture output
result = subprocess.run(
['shortcuts', 'run', 'Ask ChatGPT on Mac', '-i', prompt],
capture_output=True,
text=True,
check=True
)
return {
"operation": "ask_chatgpt",
"status": "success",
"message": result.stdout.strip()
}
else:
# Run without waiting for output
subprocess.Popen(
['shortcuts', 'run', 'Ask ChatGPT on Mac', '-i', prompt],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
return {
"operation": "ask_chatgpt",
"status": "success",
"message": "Sent to ChatGPT (not waiting for response)"
}
except subprocess.CalledProcessError as e:
return {
"operation": "ask_chatgpt",
"status": "error",
"message": f"Shortcuts command failed: {e.stderr.strip() if e.stderr else str(e)}"
}
except Exception as e:
return {
"operation": "ask_chatgpt",
"status": "error",
"message": f"Unexpected error: {str(e)}"
}
def main():
"""Entry point for the MCP server."""
mcp.run()
if __name__ == "__main__":
main()
```