# 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:
--------------------------------------------------------------------------------
```
  1 | # Byte-compiled / optimized / DLL files
  2 | __pycache__/
  3 | *.py[cod]
  4 | *$py.class
  5 | 
  6 | # C extensions
  7 | *.so
  8 | 
  9 | # Distribution / packaging
 10 | .Python
 11 | build/
 12 | develop-eggs/
 13 | dist/
 14 | downloads/
 15 | eggs/
 16 | .eggs/
 17 | lib/
 18 | lib64/
 19 | parts/
 20 | sdist/
 21 | var/
 22 | wheels/
 23 | share/python-wheels/
 24 | *.egg-info/
 25 | .installed.cfg
 26 | *.egg
 27 | MANIFEST
 28 | 
 29 | # PyInstaller
 30 | #  Usually these files are written by a python script from a template
 31 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
 32 | *.manifest
 33 | *.spec
 34 | 
 35 | # Installer logs
 36 | pip-log.txt
 37 | pip-delete-this-directory.txt
 38 | 
 39 | # Unit test / coverage reports
 40 | htmlcov/
 41 | .tox/
 42 | .nox/
 43 | .coverage
 44 | .coverage.*
 45 | .cache
 46 | nosetests.xml
 47 | coverage.xml
 48 | *.cover
 49 | *.py,cover
 50 | .hypothesis/
 51 | .pytest_cache/
 52 | cover/
 53 | 
 54 | # Translations
 55 | *.mo
 56 | *.pot
 57 | 
 58 | # Django stuff:
 59 | *.log
 60 | local_settings.py
 61 | db.sqlite3
 62 | db.sqlite3-journal
 63 | 
 64 | # Flask stuff:
 65 | instance/
 66 | .webassets-cache
 67 | 
 68 | # Scrapy stuff:
 69 | .scrapy
 70 | 
 71 | # Sphinx documentation
 72 | docs/_build/
 73 | 
 74 | # PyBuilder
 75 | .pybuilder/
 76 | target/
 77 | 
 78 | # Jupyter Notebook
 79 | .ipynb_checkpoints
 80 | 
 81 | # IPython
 82 | profile_default/
 83 | ipython_config.py
 84 | 
 85 | # pyenv
 86 | #   For a library or package, you might want to ignore these files since the code is
 87 | #   intended to run in multiple environments; otherwise, check them in:
 88 | # .python-version
 89 | 
 90 | # pipenv
 91 | #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 92 | #   However, in case of collaboration, if having platform-specific dependencies or dependencies
 93 | #   having no cross-platform support, pipenv may install dependencies that don't work, or not
 94 | #   install all needed dependencies.
 95 | #Pipfile.lock
 96 | 
 97 | # UV
 98 | #   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
 99 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
100 | #   commonly ignored for libraries.
101 | #uv.lock
102 | 
103 | # poetry
104 | #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
106 | #   commonly ignored for libraries.
107 | #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 | 
110 | # pdm
111 | #   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112 | #pdm.lock
113 | #   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114 | #   in version control.
115 | #   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116 | .pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 | 
120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121 | __pypackages__/
122 | 
123 | # Celery stuff
124 | celerybeat-schedule
125 | celerybeat.pid
126 | 
127 | # SageMath parsed files
128 | *.sage.py
129 | 
130 | # Environments
131 | .env
132 | .venv
133 | env/
134 | venv/
135 | ENV/
136 | env.bak/
137 | venv.bak/
138 | 
139 | # Spyder project settings
140 | .spyderproject
141 | .spyproject
142 | 
143 | # Rope project settings
144 | .ropeproject
145 | 
146 | # mkdocs documentation
147 | /site
148 | 
149 | # mypy
150 | .mypy_cache/
151 | .dmypy.json
152 | dmypy.json
153 | 
154 | # Pyre type checker
155 | .pyre/
156 | 
157 | # pytype static type analyzer
158 | .pytype/
159 | 
160 | # Cython debug symbols
161 | cython_debug/
162 | 
163 | # PyCharm
164 | #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165 | #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166 | #  and can be added to the global gitignore or merged into this file.  For a more nuclear
167 | #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
168 | #.idea/
169 | 
170 | # Ruff stuff:
171 | .ruff_cache/
172 | 
173 | # PyPI configuration file
174 | .pypirc
175 | 
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
 1 | # mcp-server-chatgpt-app
 2 | 
 3 | ## Prerequisite
 4 | 
 5 | - [ChatGPT macOS app](https://openai.com/chatgpt/download/)
 6 | - [uv](https://docs.astral.sh/uv/getting-started/installation/)
 7 | - Install the ["Ask ChatGPT on Mac" shortcuts](https://www.icloud.com/shortcuts/6ae86a39a31e4ec5938abad953ecfd64)
 8 | 
 9 | ## Usage
10 | 
11 | ### cursor
12 | 
13 | update `.mcp.json` to add the following:
14 | 
15 | ```
16 | {
17 |     "mcpServers": {
18 |       "chatgpt": {
19 |         "command": "uvx",
20 |         "args": ["mcp-server-chatgpt-app"],
21 |         "env": {},
22 |         "disabled": false,
23 |         "autoApprove": []
24 |       }
25 |     }
26 | }
27 | ```
28 | 
29 | ### chatwise
30 | 
31 | Go to Settings -> Tools -> Add and use the following config:
32 | 
33 | ```
34 | Type: stdio
35 | ID: ChatGPT
36 | Command: uvx mcp-server-chatgpt-app
37 | ```
38 | 
39 | > [!CAUTION]
40 | > Chatwise did not close mcp server even when itself is closed, which may lead to multiple mcp servers running at the same time.
41 | > DO remember to clean them all up: `pkill -f 'mcp-server-chatgpt-app'`
42 | 
43 | ### [MCP Inspector](https://github.com/modelcontextprotocol/inspector)
44 | 
45 | ```
46 | Transport type: stdio
47 | Command: uv
48 | Arguments: --directory /Users/<your-username>/Developer/mcp-server-chatgpt-app/src/mcp_server_chatgpt run server.py
49 | Configuration/Request Timeout: 100000
50 | ```
51 | 
52 | 
53 | ## local development
54 | 
55 | ```
56 | uv --directory $HOME/Developer/mcp-server-chatgpt-app/src/mcp_server_chatgpt run server.py
57 | ```
58 | 
59 | 
```
--------------------------------------------------------------------------------
/src/mcp_server_chatgpt/__init__.py:
--------------------------------------------------------------------------------
```python
1 | from .server import main
2 | 
3 | __all__ = ['main'] 
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
 1 | [project]
 2 | name = "mcp-server-chatgpt-app"
 3 | version = "0.1.5"
 4 | description = "MCP Server for ChatGPT via AppleScript"
 5 | readme = "README.md"
 6 | requires-python = ">=3.10"
 7 | license = "MIT"
 8 | authors = [
 9 |     {name = "cdpath", email = "[email protected]"}
10 | ]
11 | keywords = ["ChatGPT", "mcp", "AppleScript"]
12 | classifiers = [
13 |     "Development Status :: 3 - Alpha",
14 |     "Intended Audience :: Developers",
15 |     "License :: OSI Approved :: MIT License",
16 |     "Programming Language :: Python :: 3",
17 |     "Programming Language :: Python :: 3.10",
18 | ]
19 | dependencies = [
20 |     "mcp>=1.6.0",
21 | ]
22 | 
23 | [project.urls]
24 | Homepage = "https://github.com/cdpath/mcp-server-chatgpt-app"
25 | Repository = "https://github.com/cdpath/mcp-server-chatgpt-app.git"
26 | Issues = "https://github.com/cdpath/mcp-server-chatgpt-app/issues"
27 | 
28 | [build-system]
29 | requires = ["hatchling"]
30 | build-backend = "hatchling.build"
31 | 
32 | [tool.hatch.build.targets.wheel]
33 | packages = ["src/mcp_server_chatgpt"]
34 | 
35 | [project.scripts]
36 | mcp-server-chatgpt-app = "mcp_server_chatgpt.server:main"
37 | 
```
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
```yaml
 1 | # This workflow will upload a Python Package to PyPI when a release is created
 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
 3 | 
 4 | # This workflow uses actions that are not certified by GitHub.
 5 | # They are provided by a third-party and are governed by
 6 | # separate terms of service, privacy policy, and support
 7 | # documentation.
 8 | 
 9 | name: Upload Python Package
10 | 
11 | on:
12 |   release:
13 |     types: [published]
14 | 
15 | permissions:
16 |   contents: read
17 | 
18 | jobs:
19 |   release-build:
20 |     runs-on: ubuntu-latest
21 | 
22 |     steps:
23 |       - uses: actions/checkout@v4
24 | 
25 |       - uses: actions/setup-python@v5
26 |         with:
27 |           python-version: "3.x"
28 | 
29 |       - name: Build release distributions
30 |         run: |
31 |           # NOTE: put your own distribution build steps here.
32 |           python -m pip install build
33 |           python -m build
34 | 
35 |       - name: Upload distributions
36 |         uses: actions/upload-artifact@v4
37 |         with:
38 |           name: release-dists
39 |           path: dist/
40 | 
41 |   pypi-publish:
42 |     runs-on: ubuntu-latest
43 |     needs:
44 |       - release-build
45 |     permissions:
46 |       # IMPORTANT: this permission is mandatory for trusted publishing
47 |       id-token: write
48 | 
49 |     # Dedicated environments with protections for publishing are strongly recommended.
50 |     # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
51 |     environment:
52 |       name: pypi
53 |       # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
54 |       # url: https://pypi.org/p/YOURPROJECT
55 |       #
56 |       # ALTERNATIVE: if your GitHub Release name is the PyPI project version string
57 |       # ALTERNATIVE: exactly, uncomment the following line instead:
58 |       # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
59 | 
60 |     steps:
61 |       - name: Retrieve release distributions
62 |         uses: actions/download-artifact@v4
63 |         with:
64 |           name: release-dists
65 |           path: dist/
66 | 
67 |       - name: Publish release distributions to PyPI
68 |         uses: pypa/gh-action-pypi-publish@release/v1
69 |         with:
70 |           packages-dir: dist/
71 | 
```
--------------------------------------------------------------------------------
/src/mcp_server_chatgpt/server.py:
--------------------------------------------------------------------------------
```python
  1 | import subprocess
  2 | import os
  3 | from typing import Optional, Dict, Any
  4 | from mcp.server.fastmcp import FastMCP
  5 | 
  6 | mcp = FastMCP("ChatGPT")
  7 | 
  8 | def run_applescript(script: str, wait_for_output: bool = True) -> Dict[str, Any]:
  9 |     """
 10 |     Run an AppleScript and return its output and status.
 11 |     
 12 |     Args:
 13 |         script: The AppleScript to run
 14 |         wait_for_output: Whether to wait for and capture output
 15 |     """
 16 |     try:
 17 |         if wait_for_output:
 18 |             # Run synchronously and capture output
 19 |             result = subprocess.run(
 20 |                 ['osascript', '-e', script],
 21 |                 capture_output=True,
 22 |                 text=True,
 23 |                 check=True
 24 |             )
 25 |             return {
 26 |                 "success": True,
 27 |                 "output": result.stdout.strip()
 28 |             }
 29 |         else:
 30 |             # Use Popen for non-blocking execution
 31 |             subprocess.Popen(
 32 |                 ['osascript', '-e', script], 
 33 |                 stdout=subprocess.DEVNULL,
 34 |                 stderr=subprocess.DEVNULL,
 35 |                 start_new_session=True
 36 |             )
 37 |             return {
 38 |                 "success": True,
 39 |                 "output": ""
 40 |             }
 41 |     except Exception as e:
 42 |         error_msg = str(e)
 43 |         return {
 44 |             "success": False,
 45 |             "error": error_msg,
 46 |         }
 47 | 
 48 | @mcp.tool()
 49 | def ask_chatgpt(prompt: str, wait_for_output: bool = False) -> Dict[str, Any]:
 50 |     """
 51 |     Send a prompt to ChatGPT macOS app using Shortcuts.
 52 |     
 53 |     Args:
 54 |         prompt: The text to send to ChatGPT
 55 |         wait_for_output: Whether to wait for ChatGPT to respond
 56 |     Returns:
 57 |         Dict containing operation status and response
 58 |     """
 59 |     try:
 60 |         if wait_for_output:
 61 |             # Use shortcuts command directly to capture output
 62 |             result = subprocess.run(
 63 |                 ['shortcuts', 'run', 'Ask ChatGPT on Mac', '-i', prompt],
 64 |                 capture_output=True,
 65 |                 text=True,
 66 |                 check=True
 67 |             )
 68 |             return {
 69 |                 "operation": "ask_chatgpt",
 70 |                 "status": "success",
 71 |                 "message": result.stdout.strip()
 72 |             }
 73 |         else:
 74 |             # Run without waiting for output
 75 |             subprocess.Popen(
 76 |                 ['shortcuts', 'run', 'Ask ChatGPT on Mac', '-i', prompt],
 77 |                 stdout=subprocess.DEVNULL,
 78 |                 stderr=subprocess.DEVNULL,
 79 |                 start_new_session=True
 80 |             )
 81 |             return {
 82 |                 "operation": "ask_chatgpt",
 83 |                 "status": "success",
 84 |                 "message": "Sent to ChatGPT (not waiting for response)"
 85 |             }
 86 |     except subprocess.CalledProcessError as e:
 87 |         return {
 88 |             "operation": "ask_chatgpt",
 89 |             "status": "error",
 90 |             "message": f"Shortcuts command failed: {e.stderr.strip() if e.stderr else str(e)}"
 91 |         }
 92 |     except Exception as e:
 93 |         return {
 94 |             "operation": "ask_chatgpt",
 95 |             "status": "error",
 96 |             "message": f"Unexpected error: {str(e)}"
 97 |         }
 98 | 
 99 | def main():
100 |     """Entry point for the MCP server."""
101 |     mcp.run()
102 | 
103 | if __name__ == "__main__":
104 |     main() 
```