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.
- linode_oauth_mcp_server-0.1.0/.github/workflows/ci.yml +87 -0
- linode_oauth_mcp_server-0.1.0/.github/workflows/release.yml +56 -0
- linode_oauth_mcp_server-0.1.0/.gitignore +223 -0
- linode_oauth_mcp_server-0.1.0/Dockerfile +7 -0
- linode_oauth_mcp_server-0.1.0/LICENSE +21 -0
- linode_oauth_mcp_server-0.1.0/PKG-INFO +190 -0
- linode_oauth_mcp_server-0.1.0/README.md +155 -0
- linode_oauth_mcp_server-0.1.0/pyproject.toml +72 -0
- linode_oauth_mcp_server-0.1.0/setup.cfg +4 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/__init__.py +10 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/auth.py +407 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/config.py +168 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/data/config/apis.yaml +16 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/main.py +421 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/oauth_scopes.py +121 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/openapi_annotations.py +102 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/response_formatter.py +215 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/search.py +121 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/spec_manager.py +783 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server/tool_summaries.py +136 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/PKG-INFO +190 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/SOURCES.txt +32 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/dependency_links.txt +1 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/entry_points.txt +2 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/requires.txt +18 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/scm_file_list.json +28 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/scm_version.json +8 -0
- linode_oauth_mcp_server-0.1.0/src/linode_oauth_mcp_server.egg-info/top_level.txt +1 -0
- linode_oauth_mcp_server-0.1.0/tests/conftest.py +48 -0
- linode_oauth_mcp_server-0.1.0/tests/test_oauth_scopes.py +222 -0
- linode_oauth_mcp_server-0.1.0/tests/test_openapi_annotations.py +159 -0
- linode_oauth_mcp_server-0.1.0/tests/test_sign_in_flow.py +225 -0
- linode_oauth_mcp_server-0.1.0/tests/test_spec_cache.py +71 -0
- 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,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)
|