linode-oauth-mcp-server 0.1.0__tar.gz

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.
Files changed (34) hide show
  1. linode_oauth_mcp_server-0.1.0/.github/workflows/ci.yml +87 -0
  2. linode_oauth_mcp_server-0.1.0/.github/workflows/release.yml +56 -0
  3. linode_oauth_mcp_server-0.1.0/.gitignore +223 -0
  4. linode_oauth_mcp_server-0.1.0/Dockerfile +7 -0
  5. linode_oauth_mcp_server-0.1.0/LICENSE +21 -0
  6. linode_oauth_mcp_server-0.1.0/PKG-INFO +190 -0
  7. linode_oauth_mcp_server-0.1.0/README.md +155 -0
  8. linode_oauth_mcp_server-0.1.0/pyproject.toml +72 -0
  9. linode_oauth_mcp_server-0.1.0/setup.cfg +4 -0
  10. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/__init__.py +10 -0
  11. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/auth.py +407 -0
  12. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/config.py +168 -0
  13. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/data/config/apis.yaml +16 -0
  14. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/main.py +421 -0
  15. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/oauth_scopes.py +121 -0
  16. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/openapi_annotations.py +102 -0
  17. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/response_formatter.py +215 -0
  18. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/search.py +121 -0
  19. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/spec_manager.py +783 -0
  20. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/tool_summaries.py +136 -0
  21. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/PKG-INFO +190 -0
  22. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/SOURCES.txt +32 -0
  23. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/dependency_links.txt +1 -0
  24. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/entry_points.txt +2 -0
  25. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/requires.txt +18 -0
  26. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/scm_file_list.json +28 -0
  27. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/scm_version.json +8 -0
  28. linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/top_level.txt +1 -0
  29. linode_oauth_mcp_server-0.1.0/tests/conftest.py +48 -0
  30. linode_oauth_mcp_server-0.1.0/tests/test_oauth_scopes.py +222 -0
  31. linode_oauth_mcp_server-0.1.0/tests/test_openapi_annotations.py +159 -0
  32. linode_oauth_mcp_server-0.1.0/tests/test_sign_in_flow.py +225 -0
  33. linode_oauth_mcp_server-0.1.0/tests/test_spec_cache.py +71 -0
  34. linode_oauth_mcp_server-0.1.0/tests/test_tool_output.py +189 -0
@@ -0,0 +1,87 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, develop ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ['3.13']
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v4
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: |
26
+ python -m pip install --upgrade pip
27
+ pip install -e '.[dev]'
28
+
29
+ - name: Lint with flake8
30
+ run: |
31
+ flake8 src tests --count --select=E9,F63,F7,F82 --show-source --statistics
32
+ flake8 src tests --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics
33
+
34
+ - name: Format check with black
35
+ run: |
36
+ black --check src tests
37
+
38
+ - name: Import sort check with isort
39
+ run: |
40
+ isort --check-only src tests
41
+
42
+ - name: Type check with mypy
43
+ run: |
44
+ mypy src
45
+
46
+ - name: Test with pytest
47
+ run: |
48
+ pytest tests/ --cov=src --cov-report=xml
49
+
50
+ - name: Upload coverage to Codecov
51
+ uses: codecov/codecov-action@v3
52
+ continue-on-error: true
53
+ with:
54
+ file: ./coverage.xml
55
+ fail_ci_if_error: false
56
+
57
+ validate-config:
58
+ runs-on: ubuntu-latest
59
+
60
+ steps:
61
+ - uses: actions/checkout@v4
62
+
63
+ - name: Set up Python
64
+ uses: actions/setup-python@v4
65
+ with:
66
+ python-version: '3.13'
67
+
68
+ - name: Install dependencies
69
+ run: |
70
+ python -m pip install --upgrade pip
71
+ pip install -e .[dev]
72
+
73
+ - name: Validate configuration
74
+ run: |
75
+ python -c "from linode_oauth_mcp_server.config import Config; Config.load()"
76
+ echo "✅ Packaged configuration is valid"
77
+
78
+ - name: Check spec manager setup
79
+ run: |
80
+ python -c "
81
+ from linode_oauth_mcp_server.config import Config
82
+ from linode_oauth_mcp_server.spec_manager import SpecManager
83
+ config = Config.load()
84
+ manager = SpecManager(config)
85
+ assert config.get_api_names() == ['linode']
86
+ print('SpecManager initialization successful')
87
+ "
@@ -0,0 +1,56 @@
1
+ name: Release
2
+
3
+ # Pushing a tag like v0.2.0 (or publishing a GitHub release, which creates
4
+ # the tag) builds that version and publishes it to PyPI.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ with:
15
+ # setuptools-scm derives the package version from the tag.
16
+ fetch-depth: 0
17
+
18
+ - uses: astral-sh/setup-uv@v6
19
+ with:
20
+ python-version: "3.13"
21
+
22
+ - name: Test
23
+ run: uv run --extra dev pytest -q
24
+
25
+ - name: Build
26
+ run: uv build
27
+
28
+ - name: Check version matches tag
29
+ run: |
30
+ expected="${GITHUB_REF_NAME#v}"
31
+ ls dist/
32
+ test -f "dist/linode_oauth_mcp_server-${expected}-py3-none-any.whl"
33
+
34
+ - uses: actions/upload-artifact@v4
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+
39
+ publish:
40
+ needs: build
41
+ runs-on: ubuntu-latest
42
+ # Must match the environment named in the PyPI trusted publisher.
43
+ environment:
44
+ name: pypi
45
+ url: https://pypi.org/p/linode-oauth-mcp-server
46
+ permissions:
47
+ # Lets the job request a GitHub OIDC token, which PyPI exchanges for a
48
+ # short-lived upload token. No PyPI API token is stored in the repo.
49
+ id-token: write
50
+ steps:
51
+ - uses: actions/download-artifact@v4
52
+ with:
53
+ name: dist
54
+ path: dist/
55
+
56
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,223 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ claude_desktop_config.json
7
+ mcp-config.json
8
+ linode-mcp-config.json
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py.cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # UV
102
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ #uv.lock
106
+
107
+ # poetry
108
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
109
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
110
+ # commonly ignored for libraries.
111
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
112
+ #poetry.lock
113
+ #poetry.toml
114
+
115
+ # pdm
116
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
117
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
118
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
119
+ #pdm.lock
120
+ #pdm.toml
121
+ .pdm-python
122
+ .pdm-build/
123
+
124
+ # pixi
125
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
126
+ #pixi.lock
127
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
128
+ # in the .venv directory. It is recommended not to include this directory in version control.
129
+ .pixi
130
+
131
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
132
+ __pypackages__/
133
+
134
+ # Celery stuff
135
+ celerybeat-schedule
136
+ celerybeat.pid
137
+
138
+ # SageMath parsed files
139
+ *.sage.py
140
+
141
+ # Environments
142
+ .env
143
+ .envrc
144
+ .venv
145
+ env/
146
+ venv/
147
+ ENV/
148
+ env.bak/
149
+ venv.bak/
150
+
151
+ # Spyder project settings
152
+ .spyderproject
153
+ .spyproject
154
+
155
+ # Rope project settings
156
+ .ropeproject
157
+
158
+ # mkdocs documentation
159
+ /site
160
+
161
+ # mypy
162
+ .mypy_cache/
163
+ .dmypy.json
164
+ dmypy.json
165
+
166
+ # Pyre type checker
167
+ .pyre/
168
+
169
+ # pytype static type analyzer
170
+ .pytype/
171
+
172
+ # Cython debug symbols
173
+ cython_debug/
174
+
175
+ # PyCharm
176
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
177
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
178
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
179
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
180
+ #.idea/
181
+
182
+ # Abstra
183
+ # Abstra is an AI-powered process automation framework.
184
+ # Ignore directories containing user credentials, local state, and settings.
185
+ # Learn more at https://abstra.io/docs
186
+ .abstra/
187
+
188
+ # Visual Studio Code
189
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
190
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
191
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
192
+ # you could uncomment the following to ignore the entire vscode folder
193
+ # .vscode/
194
+
195
+ # Ruff stuff:
196
+ .ruff_cache/
197
+
198
+ # PyPI configuration file
199
+ .pypirc
200
+
201
+ # Cursor
202
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
203
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
204
+ # refer to https://docs.cursor.com/context/ignore-files
205
+ .cursorignore
206
+ .cursorindexingignore
207
+
208
+ # Marimo
209
+ marimo/_static/
210
+ marimo/_lsp/
211
+ __marimo__/
212
+
213
+ # Linode MCP Server specific files
214
+ # Cache directories for specs and docs
215
+ cache/
216
+ .uv-cache/
217
+ .linode-mcp-cache/
218
+ uv.lock
219
+
220
+ # Temporary files
221
+ *.tmp
222
+ *.bak
223
+ .worktrees/
@@ -0,0 +1,7 @@
1
+ FROM python:3.13-slim
2
+
3
+ WORKDIR /app
4
+ COPY . .
5
+ RUN pip install -e .
6
+
7
+ CMD ["linode-oauth-mcp-server"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Marques Johansson
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.
@@ -0,0 +1,190 @@
1
+ Metadata-Version: 2.4
2
+ Name: linode-oauth-mcp-server
3
+ Version: 0.1.0
4
+ Summary: Linode API v4 MCP server with per-tool OAuth scopes and in-client sign-in
5
+ Author: Marques Johansson
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/displague/linode-oauth-mcp-server
8
+ Project-URL: Issues, https://github.com/displague/linode-oauth-mcp-server/issues
9
+ Keywords: linode,mcp,model-context-protocol,oauth,openapi
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Topic :: System :: Systems Administration
14
+ Requires-Python: >=3.13
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: fastmcp~=4.1.0
18
+ Requires-Dist: mcp-types
19
+ Requires-Dist: httpx2>=2.5.0
20
+ Requires-Dist: pydantic>=2.0.0
21
+ Requires-Dist: pyyaml>=6.0.0
22
+ Requires-Dist: click>=8.0.0
23
+ Requires-Dist: jq>=1.6.0
24
+ Requires-Dist: platformdirs>=4.0.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
28
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
29
+ Requires-Dist: black>=23.0.0; extra == "dev"
30
+ Requires-Dist: isort>=5.12.0; extra == "dev"
31
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
32
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
33
+ Requires-Dist: types-PyYAML; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # Linode OAuth MCP Server
37
+
38
+ An experimental MCP server that turns the Linode API v4 OpenAPI specification into tools. Each tool requests only the OAuth scopes its operation needs, and the sign-in link appears in the MCP client's chat. The API spec is fetched from Linode's official [OpenAPI distribution](https://github.com/linode/linode-api-openapi), parsed, cached on disk, and used to build the MCP tool catalog.
39
+
40
+ ## Scope and discovery
41
+
42
+ The server exposes Linode API operations as `linode_<operationId>` tools. By default, the server lists only three tools and keeps the rest of the catalog out of the initial list: `search_tools` finds operations, `call_tool` invokes them, and `get_tool_schema` returns one tool's full input schema. Search ranks tools by their name, title, and summary, and returns a compact entry for each of up to 10 matches: its title, first description paragraph, access level, required OAuth scopes, and a one-line summary per parameter. Results run about 5K characters, where full tool definitions would be up to 250K. Responses are rendered as YAML, including calls made through `call_tool`. `--tool-catalog full` lists every generated tool directly. Every paginated list tool accepts an `X-Filter` argument, a JSON string sent as Linode's [filter header](https://techdocs.akamai.com/linode-api/reference/filtering-and-sorting) (for example, `{"mine": true}`). The upstream spec declares that header on only a few operations.
43
+
44
+ The server fetches the spec on first launch and stores it in the user's cache directory (`LINODE_MCP_CACHE_DIR` overrides the default). Later starts load the cached JSON copy, which takes about a second. After startup, the server checks the upstream spec in the background with a conditional GET using the cached `ETag`. An unchanged spec costs one small `304 Not Modified` request. If the spec has changed, the server fetches and caches it, and the new spec takes effect the next time the server starts. Run `linode-oauth-mcp-server --update-specs` to force a fresh fetch.
45
+
46
+ ## Authentication
47
+
48
+ Credentials are checked in this order:
49
+
50
+ 1. `LINODE_API_TOKEN` or `LINODE_OAUTH_TOKEN`.
51
+ 2. `LINODE_CLI_TOKEN`, then the `token` in `~/.config/linode-cli` (or `LINODE_CLI_CONFIG`).
52
+ 3. A Linode public OAuth app, when `LINODE_OAUTH_CLIENT_ID` is configured.
53
+
54
+ When only the public client is configured, the first authenticated tool call opens Linode sign-in in a browser. The user grants access and the server receives the short-lived bearer token through a loopback callback. The server saves the token, its expiry, and its granted scopes to `oauth-token.json` in the cache directory. The file is readable only by the user (mode `0600`). A restarted server, for example after an MCP reconnect, reuses that token until it expires; Linode's public OAuth tokens last 2 hours. Expired tokens, and tokens saved for a different OAuth client ID, are discarded. Delete the file to sign out. Tokens from `LINODE_API_TOKEN` or linode-cli take precedence over a saved sign-in. A personal access token or an existing CLI credential is simpler for personal use. The Linode CLI can set up its own browser-based sign-in with `linode-cli configure`.
55
+
56
+ The first sign-in requests read-only access to Linodes and StackScripts, plus whatever scopes the first tool call needs. Each operation's required scopes come from its `security` entry in the OpenAPI spec. When a later call needs a scope the current token lacks (for example, `linodes:read_write` to create a Linode), the server asks for sign-in again. That request asks for the scopes already granted plus the new ones. MCP clients that support URL-mode elicitation show the sign-in link in the chat. On MCP 2026-07-28, the server returns an input-required result carrying the link, and the client retries the call after the user opens it. On earlier protocol versions, the server sends the elicitation request during the call. Clients without URL elicitation get the link opened in the system browser. Each tool's required scopes are listed in its `linode/oauthScopes` metadata. The call waits until the user approves, then continues with the new token. Operations the spec marks as public, such as listing regions or Linode types, never prompt for sign-in. Set `LINODE_OAUTH_SCOPES` (comma- or space-separated) to change the initial scope set, for example to `*` to grant everything up front. Tokens from `LINODE_API_TOKEN` or linode-cli are used as-is, without scope checks.
57
+
58
+ The repo includes a shared public OAuth client ID. To register a replacement client, use a Linode CLI identity permitted to create OAuth clients:
59
+
60
+ ```sh
61
+ linode-cli account client-create \
62
+ --label "Linode OAuth MCP Server" \
63
+ --redirect_uri http://localhost:8765/oauth/callback \
64
+ --public true
65
+ ```
66
+
67
+ The returned `id` is public and safe to commit; do not publish a private client's secret. Users can share this public client and do not need to register their own. The callback URL must exactly match the configured `http://localhost:8765/oauth/callback`. Public OAuth returns a short-lived access token in the redirect URL and does not issue a refresh token, so users sign in again when it expires. For personal use, a Linode CLI credential or personal access token is simpler. See [Linode authentication docs](https://techdocs.akamai.com/linode-api/reference/get-started) and [Create an OAuth client](https://techdocs.akamai.com/linode-api/reference/post-client).
68
+
69
+ ## Install and run
70
+
71
+ Requires Python 3.13+. The package is published on PyPI as [`linode-oauth-mcp-server`](https://pypi.org/p/linode-oauth-mcp-server). Run it without installing, using either tool:
72
+
73
+ ```sh
74
+ uvx linode-oauth-mcp-server
75
+ pipx run linode-oauth-mcp-server
76
+ ```
77
+
78
+ Both fetch the latest release on first use and cache it. To pick up a newer release, run `uvx linode-oauth-mcp-server@latest`, or pin a version with `uvx linode-oauth-mcp-server@0.2.0`. To install the command permanently, run `uv tool install linode-oauth-mcp-server` or `pipx install linode-oauth-mcp-server`.
79
+
80
+ This starts the default stdio server for MCP clients that launch local processes. For ChatGPT, which connects to remote MCP endpoints, run the server over local Streamable HTTP and connect through a Secure MCP Tunnel:
81
+
82
+ ```sh
83
+ uvx linode-oauth-mcp-server --transport http --host 127.0.0.1 --port 8000
84
+ ```
85
+
86
+ The local MCP endpoint is `http://127.0.0.1:8000/mcp`. In ChatGPT, create a custom MCP connection and select an available Secure MCP Tunnel for this endpoint; use the resulting tunnel URL in the connection. Keep the local server running while testing. The HTTP endpoint has no separate MCP bearer authentication, so use it only locally behind a private tunnel; do not expose it directly to the public internet.
87
+
88
+ The initial spec fetch requires network access. Later starts use the cached specification.
89
+
90
+ ## MCP client examples
91
+
92
+ The server signs in through Linode's shared public OAuth client by default, so no token needs to be copied into client configuration. It also uses `LINODE_API_TOKEN` or an existing Linode CLI credential when one is present.
93
+
94
+ ### Claude Code
95
+
96
+ ```sh
97
+ claude mcp add linode -- uvx linode-oauth-mcp-server
98
+ ```
99
+
100
+ Run `/mcp` in Claude Code to confirm that `linode` is connected.
101
+
102
+ ### Codex CLI
103
+
104
+ ```sh
105
+ codex mcp add linode -- uvx linode-oauth-mcp-server
106
+ ```
107
+
108
+ If your Codex sandbox restricts writes to your home cache directory or blocks network access, use local caches and fetch the spec once from your normal shell before adding the server:
109
+
110
+ ```sh
111
+ UV_CACHE_DIR="$PWD/.uv-cache" LINODE_MCP_CACHE_DIR="$PWD/.linode-mcp-cache" \
112
+ uvx linode-oauth-mcp-server --update-specs
113
+ codex mcp add linode \
114
+ --env UV_CACHE_DIR="$PWD/.uv-cache" \
115
+ --env LINODE_MCP_CACHE_DIR="$PWD/.linode-mcp-cache" \
116
+ -- uvx linode-oauth-mcp-server
117
+ ```
118
+
119
+ Run `codex mcp list` to confirm the registration, then restart Codex so the server is loaded.
120
+
121
+ ### Other stdio clients
122
+
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "linode": {
127
+ "command": "uvx",
128
+ "args": ["linode-oauth-mcp-server"]
129
+ }
130
+ }
131
+ }
132
+ ```
133
+
134
+ ## API spec updates
135
+
136
+ The default source is `https://raw.githubusercontent.com/linode/linode-api-openapi/main/openapi.json`. To change it, pass a YAML config:
137
+
138
+ ```yaml
139
+ apis:
140
+ linode:
141
+ description: "Linode API v4"
142
+ tags: [cloud, compute, networking]
143
+ service_name: linode
144
+ specs:
145
+ - url: "https://raw.githubusercontent.com/linode/linode-api-openapi/main/openapi.json"
146
+ ```
147
+
148
+ Then run `linode-oauth-mcp-server --config path/to/apis.yaml --update-specs`.
149
+
150
+ ## Safety and limits
151
+
152
+ The upstream OpenAPI document includes read and write operations, including operations that create billable resources or delete resources. Review the operation description and parameters before invoking a write. Grant the token only the scopes required for the work. This project is experimental; API behavior and account-level permissions still need testing against a Linode account.
153
+
154
+ ## Development
155
+
156
+ Run the server and tests from a checkout:
157
+
158
+ ```sh
159
+ uv run linode-oauth-mcp-server
160
+ uv run --extra dev pytest
161
+ ```
162
+
163
+ To have an MCP client run your checkout instead of the PyPI release, register `uv run --project /path/to/checkout linode-oauth-mcp-server` as the command.
164
+
165
+ ## Releasing
166
+
167
+ Versions come from git tags through `setuptools-scm`. Pushing a tag like `v0.2.0`, or publishing a GitHub release that creates one, runs [`.github/workflows/release.yml`](.github/workflows/release.yml). The workflow tests, builds, and publishes `0.2.0` to PyPI:
168
+
169
+ ```sh
170
+ git tag v0.2.0
171
+ git push origin v0.2.0
172
+ ```
173
+
174
+ Publishing uses [PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/), which is based on GitHub's OpenID Connect tokens, so the repository stores no PyPI API token. One-time setup:
175
+
176
+ 1. On PyPI, open **Your account → Publishing → Add a new pending publisher** and enter:
177
+ - PyPI project name: `linode-oauth-mcp-server`
178
+ - Owner: `displague`
179
+ - Repository name: `linode-oauth-mcp-server`
180
+ - Workflow name: `release.yml`
181
+ - Environment name: `pypi`
182
+ 2. On GitHub, create an environment named `pypi` under **Settings → Environments**. Optionally, limit it to `v*` tags or require a reviewer.
183
+
184
+ The first tagged release creates the PyPI project.
185
+
186
+ ## References
187
+
188
+ - [Linode API v4 reference](https://techdocs.akamai.com/linode-api/reference/api)
189
+ - [Linode API authentication](https://techdocs.akamai.com/linode-api/reference/get-started)
190
+ - [Linode OpenAPI spec distribution](https://github.com/linode/linode-api-openapi)