coding-tools-mcp 0.1.5__tar.gz → 0.1.6__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.
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/PKG-INFO +41 -5
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/README.md +1 -1
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/coding_tools_mcp/__init__.py +1 -1
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/coding_tools_mcp/server.py +113 -47
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/coding_tools_mcp.egg-info/PKG-INFO +41 -5
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/coding_tools_mcp.egg-info/SOURCES.txt +1 -2
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/pyproject.toml +3 -3
- coding_tools_mcp-0.1.5/docs/profile-v0.1.md +0 -1139
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/LICENSE +0 -0
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/coding_tools_mcp/__main__.py +0 -0
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/coding_tools_mcp/landlock_exec.py +0 -0
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/coding_tools_mcp.egg-info/dependency_links.txt +0 -0
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/coding_tools_mcp.egg-info/entry_points.txt +0 -0
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/coding_tools_mcp.egg-info/requires.txt +0 -0
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/coding_tools_mcp.egg-info/top_level.txt +0 -0
- {coding_tools_mcp-0.1.5 → coding_tools_mcp-0.1.6}/setup.cfg +0 -0
|
@@ -1,9 +1,46 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: coding-tools-mcp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Workspace-confined coding tools exposed as an MCP server.
|
|
5
5
|
Author: Coding Tools MCP Contributors
|
|
6
|
-
License
|
|
6
|
+
License: Coding Tools MCP Source-Available License v1.0
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Coding Tools MCP Contributors.
|
|
9
|
+
All rights reserved except as expressly granted below.
|
|
10
|
+
|
|
11
|
+
1. Permitted Use
|
|
12
|
+
|
|
13
|
+
You may view, clone, build, run, and modify the Software solely for internal
|
|
14
|
+
evaluation, development, testing, and security review.
|
|
15
|
+
|
|
16
|
+
2. Restrictions
|
|
17
|
+
|
|
18
|
+
Without prior written permission from the copyright holders, you may not:
|
|
19
|
+
|
|
20
|
+
- distribute, publish, sublicense, sell, lease, or otherwise transfer the
|
|
21
|
+
Software or modified versions of the Software;
|
|
22
|
+
- provide the Software or modified versions as a hosted, managed, or
|
|
23
|
+
software-as-a-service offering for third parties;
|
|
24
|
+
- use the Software or modified versions for production commercial purposes;
|
|
25
|
+
- remove or alter copyright, license, or attribution notices;
|
|
26
|
+
- use the project name, trademarks, or branding to imply endorsement.
|
|
27
|
+
|
|
28
|
+
3. Contributions
|
|
29
|
+
|
|
30
|
+
Unless a separate written agreement says otherwise, any contribution submitted
|
|
31
|
+
to this project may be used by the copyright holders under this license and
|
|
32
|
+
under any future license chosen by the copyright holders.
|
|
33
|
+
|
|
34
|
+
4. No Warranty
|
|
35
|
+
|
|
36
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
37
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
38
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
39
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
|
40
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
|
|
41
|
+
OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
42
|
+
SOFTWARE.
|
|
43
|
+
|
|
7
44
|
Project-URL: Homepage, https://github.com/xyTom/coding-tools-mcp
|
|
8
45
|
Project-URL: Documentation, https://github.com/xyTom/coding-tools-mcp/tree/main/docs
|
|
9
46
|
Project-URL: Source, https://github.com/xyTom/coding-tools-mcp
|
|
@@ -18,7 +55,6 @@ Requires-Dist: ruff<0.16,>=0.15; extra == "dev"
|
|
|
18
55
|
Requires-Dist: typing_extensions>=4.12; extra == "dev"
|
|
19
56
|
Provides-Extra: image
|
|
20
57
|
Requires-Dist: Pillow>=10.0; extra == "image"
|
|
21
|
-
Dynamic: license-file
|
|
22
58
|
|
|
23
59
|
# Coding Tools MCP
|
|
24
60
|
|
|
@@ -184,7 +220,7 @@ scripts/tunnel.sh ngrok /path/to/repo
|
|
|
184
220
|
scripts/tunnel.sh devtunnel /path/to/repo
|
|
185
221
|
```
|
|
186
222
|
|
|
187
|
-
For clients that support custom headers, use bearer-token auth with `Authorization: Bearer <token>`. For MCP clients that speak OAuth 2.1 Authorization Code + PKCE, use `CODING_TOOLS_MCP_AUTH_MODE=oauth` with `scripts/tunnel.sh` (or `scripts/install.sh --auth-mode oauth`)
|
|
223
|
+
For clients that support custom headers, use bearer-token auth with `Authorization: Bearer <token>`. For MCP clients that speak OAuth 2.1 Authorization Code + PKCE, use `CODING_TOOLS_MCP_AUTH_MODE=oauth` with `scripts/tunnel.sh` (or `scripts/install.sh --auth-mode oauth`). The server can infer its OAuth issuer from the tunnel request URL, so one-shot tunnels like cloudflared work without setting `CODING_TOOLS_MCP_SERVER_URL` before startup; set it only when you want to pin a stable issuer. The script prints a generated OAuth password, accepts any non-empty client_id by default, and lets you opt into `CODING_TOOLS_MCP_OAUTH_CLIENT_ID`/`CODING_TOOLS_MCP_OAUTH_CLIENT_SECRET` only when you need to lock down a confidential client. Clients that cannot send custom bearer headers and do not speak OAuth should use anonymous `read-only` mode only for local/testing tunnels, or be placed behind an external auth proxy for production use.
|
|
188
224
|
|
|
189
225
|
See [docs/remote-mcp.md](docs/remote-mcp.md) for the exact modes and security notes.
|
|
190
226
|
|
|
@@ -162,7 +162,7 @@ scripts/tunnel.sh ngrok /path/to/repo
|
|
|
162
162
|
scripts/tunnel.sh devtunnel /path/to/repo
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
For clients that support custom headers, use bearer-token auth with `Authorization: Bearer <token>`. For MCP clients that speak OAuth 2.1 Authorization Code + PKCE, use `CODING_TOOLS_MCP_AUTH_MODE=oauth` with `scripts/tunnel.sh` (or `scripts/install.sh --auth-mode oauth`)
|
|
165
|
+
For clients that support custom headers, use bearer-token auth with `Authorization: Bearer <token>`. For MCP clients that speak OAuth 2.1 Authorization Code + PKCE, use `CODING_TOOLS_MCP_AUTH_MODE=oauth` with `scripts/tunnel.sh` (or `scripts/install.sh --auth-mode oauth`). The server can infer its OAuth issuer from the tunnel request URL, so one-shot tunnels like cloudflared work without setting `CODING_TOOLS_MCP_SERVER_URL` before startup; set it only when you want to pin a stable issuer. The script prints a generated OAuth password, accepts any non-empty client_id by default, and lets you opt into `CODING_TOOLS_MCP_OAUTH_CLIENT_ID`/`CODING_TOOLS_MCP_OAUTH_CLIENT_SECRET` only when you need to lock down a confidential client. Clients that cannot send custom bearer headers and do not speak OAuth should use anonymous `read-only` mode only for local/testing tunnels, or be placed behind an external auth proxy for production use.
|
|
166
166
|
|
|
167
167
|
See [docs/remote-mcp.md](docs/remote-mcp.md) for the exact modes and security notes.
|
|
168
168
|
|
|
@@ -161,10 +161,10 @@ OAUTH_MAX_BODY_BYTES = 8_192
|
|
|
161
161
|
|
|
162
162
|
@dataclass(frozen=True)
|
|
163
163
|
class OAuthConfig:
|
|
164
|
-
client_id: str
|
|
165
|
-
client_secret: str
|
|
164
|
+
client_id: str | None
|
|
165
|
+
client_secret: str | None
|
|
166
166
|
password: str
|
|
167
|
-
server_url: str
|
|
167
|
+
server_url: str | None
|
|
168
168
|
token_secret: bytes
|
|
169
169
|
token_ttl: int = OAUTH_TOKEN_TTL_SECONDS
|
|
170
170
|
|
|
@@ -175,23 +175,63 @@ def _verify_pkce(code_verifier: str, code_challenge: str) -> bool:
|
|
|
175
175
|
return secrets.compare_digest(expected, code_challenge)
|
|
176
176
|
|
|
177
177
|
|
|
178
|
-
def _create_oauth_token(cfg: OAuthConfig) -> str:
|
|
178
|
+
def _create_oauth_token(cfg: OAuthConfig, server_url: str) -> str:
|
|
179
179
|
now = int(time.time())
|
|
180
180
|
return jwt.encode(
|
|
181
|
-
{"iss":
|
|
181
|
+
{"iss": server_url, "aud": server_url, "iat": now, "exp": now + cfg.token_ttl, "scope": "mcp"},
|
|
182
182
|
cfg.token_secret,
|
|
183
183
|
algorithm="HS256",
|
|
184
184
|
)
|
|
185
185
|
|
|
186
186
|
|
|
187
|
-
def _validate_oauth_token(token: str, cfg: OAuthConfig) -> bool:
|
|
187
|
+
def _validate_oauth_token(token: str, cfg: OAuthConfig, server_url: str) -> bool:
|
|
188
188
|
try:
|
|
189
|
-
jwt.decode(token, cfg.token_secret, algorithms=["HS256"], audience=
|
|
189
|
+
jwt.decode(token, cfg.token_secret, algorithms=["HS256"], audience=server_url, issuer=server_url)
|
|
190
190
|
return True
|
|
191
191
|
except jwt.PyJWTError:
|
|
192
192
|
return False
|
|
193
193
|
|
|
194
194
|
|
|
195
|
+
def _oauth_client_id_allowed(client_id: str, cfg: OAuthConfig) -> bool:
|
|
196
|
+
if not client_id:
|
|
197
|
+
return False
|
|
198
|
+
if cfg.client_id is None:
|
|
199
|
+
return True
|
|
200
|
+
return secrets.compare_digest(client_id, cfg.client_id)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _oauth_token_auth_methods(cfg: OAuthConfig) -> list[str]:
|
|
204
|
+
if cfg.client_secret is None:
|
|
205
|
+
return ["none"]
|
|
206
|
+
return ["client_secret_post", "client_secret_basic"]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _http_base_for_bind_host(host: str, port: int) -> str:
|
|
210
|
+
if ":" in host and not host.startswith("["):
|
|
211
|
+
host = f"[{host}]"
|
|
212
|
+
return f"http://{host}:{port}"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _first_header_value(value: str | None) -> str:
|
|
216
|
+
return (value or "").split(",", 1)[0].strip()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _forwarded_header_param(value: str | None, name: str) -> str:
|
|
220
|
+
first = _first_header_value(value)
|
|
221
|
+
for part in first.split(";"):
|
|
222
|
+
key, sep, raw = part.strip().partition("=")
|
|
223
|
+
if sep and key.lower() == name:
|
|
224
|
+
return raw.strip().strip('"')
|
|
225
|
+
return ""
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _safe_external_host(host: str) -> str:
|
|
229
|
+
host = host.strip()
|
|
230
|
+
if not host or any(ch in host for ch in "\r\n/\\"):
|
|
231
|
+
return ""
|
|
232
|
+
return host
|
|
233
|
+
|
|
234
|
+
|
|
195
235
|
TOOL_PROFILE_CHOICES = ("full", "read-only", "compat-readonly-all")
|
|
196
236
|
FULL_TOOL_NAMES = (
|
|
197
237
|
"server_info",
|
|
@@ -3805,11 +3845,11 @@ def input_schemas() -> dict[str, dict[str, Any]]:
|
|
|
3805
3845
|
}
|
|
3806
3846
|
|
|
3807
3847
|
|
|
3808
|
-
def _server_card_auth(runtime: Runtime) -> dict[str, Any]:
|
|
3848
|
+
def _server_card_auth(runtime: Runtime, *, oauth_base_url: str | None = None) -> dict[str, Any]:
|
|
3809
3849
|
if runtime.oauth_enabled():
|
|
3810
3850
|
cfg = runtime.oauth_config
|
|
3811
3851
|
assert cfg is not None
|
|
3812
|
-
base = cfg.server_url.rstrip("/")
|
|
3852
|
+
base = (oauth_base_url or cfg.server_url or "").rstrip("/")
|
|
3813
3853
|
return {
|
|
3814
3854
|
"type": "oauth2",
|
|
3815
3855
|
"scheme": "Bearer",
|
|
@@ -3822,7 +3862,7 @@ def _server_card_auth(runtime: Runtime) -> dict[str, Any]:
|
|
|
3822
3862
|
return {"type": "none", "scheme": None, "header": None}
|
|
3823
3863
|
|
|
3824
3864
|
|
|
3825
|
-
def server_card_payload(runtime: Runtime) -> dict[str, Any]:
|
|
3865
|
+
def server_card_payload(runtime: Runtime, *, oauth_base_url: str | None = None) -> dict[str, Any]:
|
|
3826
3866
|
names = runtime.exposed_tool_names()
|
|
3827
3867
|
annotations = {name: tool_definition(name, tool_profile=runtime.tool_profile)["annotations"] for name in names}
|
|
3828
3868
|
read_only = [name for name in names if annotations[name].get("readOnlyHint") is True]
|
|
@@ -3839,7 +3879,7 @@ def server_card_payload(runtime: Runtime) -> dict[str, Any]:
|
|
|
3839
3879
|
"endpoint": "/mcp",
|
|
3840
3880
|
"methods": ["GET", "HEAD", "POST", "OPTIONS"],
|
|
3841
3881
|
},
|
|
3842
|
-
"auth": _server_card_auth(runtime),
|
|
3882
|
+
"auth": _server_card_auth(runtime, oauth_base_url=oauth_base_url),
|
|
3843
3883
|
"toolProfile": runtime.tool_profile,
|
|
3844
3884
|
"tools": {
|
|
3845
3885
|
"count": len(names),
|
|
@@ -3917,10 +3957,10 @@ class MCPHandler(http.server.BaseHTTPRequestHandler):
|
|
|
3917
3957
|
if not self.is_authorized():
|
|
3918
3958
|
self.send_unauthorized(head_only=head_only)
|
|
3919
3959
|
return
|
|
3920
|
-
self.send_json(server_card_payload(self.runtime), head_only=head_only)
|
|
3960
|
+
self.send_json(server_card_payload(self.runtime, oauth_base_url=self.oauth_base_url()), head_only=head_only)
|
|
3921
3961
|
return
|
|
3922
3962
|
if normalized in {"/.well-known/mcp.json", "/.well-known/mcp/server-card.json"}:
|
|
3923
|
-
self.send_json(server_card_payload(self.runtime), head_only=head_only)
|
|
3963
|
+
self.send_json(server_card_payload(self.runtime, oauth_base_url=self.oauth_base_url()), head_only=head_only)
|
|
3924
3964
|
return
|
|
3925
3965
|
self.send_json({"error": "Unknown endpoint"}, status=404, head_only=head_only)
|
|
3926
3966
|
|
|
@@ -4142,13 +4182,33 @@ class MCPHandler(http.server.BaseHTTPRequestHandler):
|
|
|
4142
4182
|
return True
|
|
4143
4183
|
if self.runtime.oauth_config is not None and header.startswith("Bearer "):
|
|
4144
4184
|
token = header[len("Bearer "):]
|
|
4145
|
-
if _validate_oauth_token(token, self.runtime.oauth_config):
|
|
4185
|
+
if _validate_oauth_token(token, self.runtime.oauth_config, self.oauth_base_url()):
|
|
4146
4186
|
return True
|
|
4147
4187
|
return False
|
|
4148
4188
|
|
|
4189
|
+
def oauth_base_url(self) -> str:
|
|
4190
|
+
cfg = self.runtime.oauth_config
|
|
4191
|
+
if cfg is not None and cfg.server_url:
|
|
4192
|
+
return cfg.server_url.rstrip("/")
|
|
4193
|
+
proto = _first_header_value(self.headers.get("X-Forwarded-Proto"))
|
|
4194
|
+
if not proto:
|
|
4195
|
+
proto = _forwarded_header_param(self.headers.get("Forwarded"), "proto")
|
|
4196
|
+
host = _safe_external_host(_first_header_value(self.headers.get("X-Forwarded-Host")))
|
|
4197
|
+
if not host:
|
|
4198
|
+
host = _safe_external_host(_forwarded_header_param(self.headers.get("Forwarded"), "host"))
|
|
4199
|
+
if not host:
|
|
4200
|
+
host = _safe_external_host(self.headers.get("Host", ""))
|
|
4201
|
+
if not host:
|
|
4202
|
+
bind_host, bind_port = self.server.server_address[:2] # type: ignore[attr-defined]
|
|
4203
|
+
host = _http_base_for_bind_host(str(bind_host), int(bind_port)).removeprefix("http://")
|
|
4204
|
+
if proto not in {"http", "https"}:
|
|
4205
|
+
host_without_port = host.rsplit(":", 1)[0].strip("[]")
|
|
4206
|
+
proto = "http" if is_loopback_bind_host(host_without_port) else "https"
|
|
4207
|
+
return f"{proto}://{host}".rstrip("/")
|
|
4208
|
+
|
|
4149
4209
|
def send_unauthorized(self, *, head_only: bool = False) -> None:
|
|
4150
4210
|
if self.runtime.oauth_config is not None:
|
|
4151
|
-
base = self.
|
|
4211
|
+
base = self.oauth_base_url()
|
|
4152
4212
|
www_auth = f'Bearer realm="coding-tools-mcp", resource_metadata="{base}/.well-known/oauth-protected-resource"'
|
|
4153
4213
|
else:
|
|
4154
4214
|
www_auth = 'Bearer realm="coding-tools-mcp"'
|
|
@@ -4164,7 +4224,7 @@ class MCPHandler(http.server.BaseHTTPRequestHandler):
|
|
|
4164
4224
|
if cfg is None:
|
|
4165
4225
|
self.send_json({"error": "OAuth not configured"}, status=404, head_only=head_only)
|
|
4166
4226
|
return
|
|
4167
|
-
base =
|
|
4227
|
+
base = self.oauth_base_url()
|
|
4168
4228
|
self.send_json(
|
|
4169
4229
|
{
|
|
4170
4230
|
"issuer": base,
|
|
@@ -4173,7 +4233,7 @@ class MCPHandler(http.server.BaseHTTPRequestHandler):
|
|
|
4173
4233
|
"response_types_supported": ["code"],
|
|
4174
4234
|
"grant_types_supported": ["authorization_code"],
|
|
4175
4235
|
"code_challenge_methods_supported": ["S256"],
|
|
4176
|
-
"token_endpoint_auth_methods_supported":
|
|
4236
|
+
"token_endpoint_auth_methods_supported": _oauth_token_auth_methods(cfg),
|
|
4177
4237
|
},
|
|
4178
4238
|
head_only=head_only,
|
|
4179
4239
|
)
|
|
@@ -4183,7 +4243,7 @@ class MCPHandler(http.server.BaseHTTPRequestHandler):
|
|
|
4183
4243
|
if cfg is None:
|
|
4184
4244
|
self.send_json({"error": "OAuth not configured"}, status=404, head_only=head_only)
|
|
4185
4245
|
return
|
|
4186
|
-
base =
|
|
4246
|
+
base = self.oauth_base_url()
|
|
4187
4247
|
self.send_json(
|
|
4188
4248
|
{"resource": base, "authorization_servers": [base], "bearer_methods_supported": ["header"]},
|
|
4189
4249
|
head_only=head_only,
|
|
@@ -4212,6 +4272,7 @@ class MCPHandler(http.server.BaseHTTPRequestHandler):
|
|
|
4212
4272
|
"</head><body>"
|
|
4213
4273
|
f"<h2>Authorize Coding Tools MCP</h2>"
|
|
4214
4274
|
f"<p>Client: <strong>{esc(client_id)}</strong></p>"
|
|
4275
|
+
f"<p>Redirect URI: <code>{esc(redirect_uri)}</code></p>"
|
|
4215
4276
|
f"{error_block}"
|
|
4216
4277
|
"<form method='POST' action='/oauth/authorize'>"
|
|
4217
4278
|
f"<input type='hidden' name='client_id' value='{esc(client_id)}'>"
|
|
@@ -4259,7 +4320,7 @@ class MCPHandler(http.server.BaseHTTPRequestHandler):
|
|
|
4259
4320
|
if _p("response_type") != "code":
|
|
4260
4321
|
self._send_html("<h2>Error</h2><p>response_type must be 'code'</p>", status=400)
|
|
4261
4322
|
return
|
|
4262
|
-
if not
|
|
4323
|
+
if not _oauth_client_id_allowed(client_id, cfg):
|
|
4263
4324
|
self._send_html("<h2>Error</h2><p>Unknown client_id</p>", status=400)
|
|
4264
4325
|
return
|
|
4265
4326
|
if code_challenge_method != "S256" or not code_challenge:
|
|
@@ -4292,7 +4353,7 @@ class MCPHandler(http.server.BaseHTTPRequestHandler):
|
|
|
4292
4353
|
state = _p("state")
|
|
4293
4354
|
password = _p("password")
|
|
4294
4355
|
|
|
4295
|
-
if not
|
|
4356
|
+
if not _oauth_client_id_allowed(client_id, cfg):
|
|
4296
4357
|
self._send_html(self._oauth_login_page(
|
|
4297
4358
|
client_id=client_id, redirect_uri=redirect_uri, code_challenge=code_challenge,
|
|
4298
4359
|
code_challenge_method=code_challenge_method, state=state, error="Invalid client",
|
|
@@ -4323,6 +4384,7 @@ class MCPHandler(http.server.BaseHTTPRequestHandler):
|
|
|
4323
4384
|
"redirect_uri": redirect_uri,
|
|
4324
4385
|
"state": state,
|
|
4325
4386
|
"expires_at": now + OAUTH_CODE_TTL_SECONDS,
|
|
4387
|
+
"server_url": self.oauth_base_url(),
|
|
4326
4388
|
}
|
|
4327
4389
|
|
|
4328
4390
|
qs = urllib.parse.urlencode({"code": code, **({"state": state} if state else {})})
|
|
@@ -4339,12 +4401,17 @@ class MCPHandler(http.server.BaseHTTPRequestHandler):
|
|
|
4339
4401
|
if cfg is None:
|
|
4340
4402
|
self.send_json({"error": "unsupported_grant_type"}, status=400)
|
|
4341
4403
|
return
|
|
4404
|
+
|
|
4405
|
+
def _err(error: str, description: str) -> None:
|
|
4406
|
+
self.log_message("OAuth token error: %s - %s", error, description)
|
|
4407
|
+
self.send_json({"error": error, "error_description": description}, status=400)
|
|
4408
|
+
|
|
4342
4409
|
body = self._read_oauth_body()
|
|
4343
4410
|
if body is None:
|
|
4344
4411
|
return
|
|
4345
4412
|
content_type = self.headers.get("Content-Type", "").split(";")[0].strip().lower()
|
|
4346
4413
|
if content_type != "application/x-www-form-urlencoded":
|
|
4347
|
-
|
|
4414
|
+
_err("invalid_request", "Content-Type must be application/x-www-form-urlencoded")
|
|
4348
4415
|
return
|
|
4349
4416
|
params = urllib.parse.parse_qs(body.decode("utf-8", errors="replace"), keep_blank_values=True)
|
|
4350
4417
|
|
|
@@ -4359,7 +4426,7 @@ class MCPHandler(http.server.BaseHTTPRequestHandler):
|
|
|
4359
4426
|
client_id = _p("client_id")
|
|
4360
4427
|
client_secret = _p("client_secret")
|
|
4361
4428
|
|
|
4362
|
-
# Also accept HTTP Basic auth for client credentials
|
|
4429
|
+
# Also accept HTTP Basic auth for client credentials.
|
|
4363
4430
|
auth_header = self.headers.get("Authorization", "")
|
|
4364
4431
|
if auth_header.startswith("Basic ") and (not client_id or not client_secret):
|
|
4365
4432
|
try:
|
|
@@ -4372,16 +4439,13 @@ class MCPHandler(http.server.BaseHTTPRequestHandler):
|
|
|
4372
4439
|
except Exception: # noqa: BLE001
|
|
4373
4440
|
pass
|
|
4374
4441
|
|
|
4375
|
-
def _err(error: str, description: str) -> None:
|
|
4376
|
-
self.send_json({"error": error, "error_description": description}, status=400)
|
|
4377
|
-
|
|
4378
4442
|
if grant_type != "authorization_code":
|
|
4379
4443
|
_err("unsupported_grant_type", "Only authorization_code is supported")
|
|
4380
4444
|
return
|
|
4381
|
-
if not
|
|
4445
|
+
if not _oauth_client_id_allowed(client_id, cfg):
|
|
4382
4446
|
_err("invalid_client", "Unknown client_id")
|
|
4383
4447
|
return
|
|
4384
|
-
if not secrets.compare_digest(client_secret, cfg.client_secret):
|
|
4448
|
+
if cfg.client_secret is not None and not secrets.compare_digest(client_secret, cfg.client_secret):
|
|
4385
4449
|
_err("invalid_client", "Invalid client_secret")
|
|
4386
4450
|
return
|
|
4387
4451
|
if not code:
|
|
@@ -4410,7 +4474,8 @@ class MCPHandler(http.server.BaseHTTPRequestHandler):
|
|
|
4410
4474
|
_err("invalid_grant", "PKCE verification failed")
|
|
4411
4475
|
return
|
|
4412
4476
|
|
|
4413
|
-
|
|
4477
|
+
server_url = str(code_data.get("server_url") or self.oauth_base_url()).rstrip("/")
|
|
4478
|
+
access_token = _create_oauth_token(cfg, server_url)
|
|
4414
4479
|
self.send_json({"access_token": access_token, "token_type": "Bearer", "expires_in": cfg.token_ttl})
|
|
4415
4480
|
|
|
4416
4481
|
def send_cors_headers(self) -> None:
|
|
@@ -4460,18 +4525,13 @@ def run_http(args: argparse.Namespace) -> int:
|
|
|
4460
4525
|
oauth_config: OAuthConfig | None = None
|
|
4461
4526
|
oauth_mode = getattr(args, "oauth_mode", False) or os.environ.get(f"{ENV_PREFIX}_OAUTH_MODE") == "1"
|
|
4462
4527
|
if oauth_mode:
|
|
4463
|
-
client_id = os.environ.get(f"{ENV_PREFIX}_OAUTH_CLIENT_ID") or
|
|
4464
|
-
client_secret = os.environ.get(f"{ENV_PREFIX}_OAUTH_CLIENT_SECRET") or
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
"CODING_TOOLS_MCP_OAUTH_CLIENT_SECRET, CODING_TOOLS_MCP_OAUTH_PASSWORD, "
|
|
4471
|
-
"and CODING_TOOLS_MCP_SERVER_URL.",
|
|
4472
|
-
file=sys.stderr,
|
|
4473
|
-
)
|
|
4474
|
-
return 2
|
|
4528
|
+
client_id = os.environ.get(f"{ENV_PREFIX}_OAUTH_CLIENT_ID") or None
|
|
4529
|
+
client_secret = os.environ.get(f"{ENV_PREFIX}_OAUTH_CLIENT_SECRET") or None
|
|
4530
|
+
env_password = os.environ.get(f"{ENV_PREFIX}_OAUTH_PASSWORD")
|
|
4531
|
+
password = env_password or secrets.token_urlsafe(32)
|
|
4532
|
+
server_url = (os.environ.get(f"{ENV_PREFIX}_SERVER_URL") or "").rstrip("/") or None
|
|
4533
|
+
if not env_password:
|
|
4534
|
+
print(f"OAuth authorize password: {password}", file=sys.stderr)
|
|
4475
4535
|
raw_secret = os.environ.get(f"{ENV_PREFIX}_OAUTH_TOKEN_SECRET") or ""
|
|
4476
4536
|
if raw_secret:
|
|
4477
4537
|
try:
|
|
@@ -4492,13 +4552,15 @@ def run_http(args: argparse.Namespace) -> int:
|
|
|
4492
4552
|
client_id=client_id,
|
|
4493
4553
|
client_secret=client_secret,
|
|
4494
4554
|
password=password,
|
|
4495
|
-
server_url=server_url
|
|
4555
|
+
server_url=server_url,
|
|
4496
4556
|
token_secret=token_secret,
|
|
4497
4557
|
token_ttl=token_ttl,
|
|
4498
4558
|
)
|
|
4499
4559
|
if auth_token:
|
|
4500
|
-
print(
|
|
4501
|
-
|
|
4560
|
+
print(
|
|
4561
|
+
"Auth: dual credentials enabled — both static bearer token and OAuth 2.1 access tokens will be accepted.",
|
|
4562
|
+
file=sys.stderr,
|
|
4563
|
+
)
|
|
4502
4564
|
|
|
4503
4565
|
if not auth_token and not oauth_config and not is_loopback_bind_host(str(args.host)):
|
|
4504
4566
|
print(
|
|
@@ -4520,8 +4582,12 @@ def run_http(args: argparse.Namespace) -> int:
|
|
|
4520
4582
|
"WARNING: --dangerously-skip-all-permissions is enabled; permission-gated operations will be auto-granted.",
|
|
4521
4583
|
file=sys.stderr,
|
|
4522
4584
|
)
|
|
4523
|
-
if oauth_config:
|
|
4524
|
-
|
|
4585
|
+
if oauth_config and runtime.auth_token:
|
|
4586
|
+
url_label = oauth_config.server_url or "dynamic request URL"
|
|
4587
|
+
auth_label = f"oauth2 + bearer enabled (server_url={url_label})"
|
|
4588
|
+
elif oauth_config:
|
|
4589
|
+
url_label = oauth_config.server_url or "dynamic request URL"
|
|
4590
|
+
auth_label = f"oauth2 enabled (server_url={url_label})"
|
|
4525
4591
|
elif runtime.auth_token:
|
|
4526
4592
|
auth_label = "bearer auth enabled"
|
|
4527
4593
|
else:
|
|
@@ -4641,8 +4707,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
4641
4707
|
default=False,
|
|
4642
4708
|
help=(
|
|
4643
4709
|
"enable OAuth 2.1 Authorization Code + PKCE; "
|
|
4644
|
-
f"
|
|
4645
|
-
|
|
4710
|
+
f"{ENV_PREFIX}_SERVER_URL is optional; when unset OAuth metadata uses the request host; "
|
|
4711
|
+
"authorize password is generated when unset; client_id/client_secret are optional"
|
|
4646
4712
|
),
|
|
4647
4713
|
)
|
|
4648
4714
|
parser.add_argument(
|
|
@@ -1,9 +1,46 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: coding-tools-mcp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Workspace-confined coding tools exposed as an MCP server.
|
|
5
5
|
Author: Coding Tools MCP Contributors
|
|
6
|
-
License
|
|
6
|
+
License: Coding Tools MCP Source-Available License v1.0
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Coding Tools MCP Contributors.
|
|
9
|
+
All rights reserved except as expressly granted below.
|
|
10
|
+
|
|
11
|
+
1. Permitted Use
|
|
12
|
+
|
|
13
|
+
You may view, clone, build, run, and modify the Software solely for internal
|
|
14
|
+
evaluation, development, testing, and security review.
|
|
15
|
+
|
|
16
|
+
2. Restrictions
|
|
17
|
+
|
|
18
|
+
Without prior written permission from the copyright holders, you may not:
|
|
19
|
+
|
|
20
|
+
- distribute, publish, sublicense, sell, lease, or otherwise transfer the
|
|
21
|
+
Software or modified versions of the Software;
|
|
22
|
+
- provide the Software or modified versions as a hosted, managed, or
|
|
23
|
+
software-as-a-service offering for third parties;
|
|
24
|
+
- use the Software or modified versions for production commercial purposes;
|
|
25
|
+
- remove or alter copyright, license, or attribution notices;
|
|
26
|
+
- use the project name, trademarks, or branding to imply endorsement.
|
|
27
|
+
|
|
28
|
+
3. Contributions
|
|
29
|
+
|
|
30
|
+
Unless a separate written agreement says otherwise, any contribution submitted
|
|
31
|
+
to this project may be used by the copyright holders under this license and
|
|
32
|
+
under any future license chosen by the copyright holders.
|
|
33
|
+
|
|
34
|
+
4. No Warranty
|
|
35
|
+
|
|
36
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
37
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
38
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
39
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
|
40
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
|
|
41
|
+
OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
42
|
+
SOFTWARE.
|
|
43
|
+
|
|
7
44
|
Project-URL: Homepage, https://github.com/xyTom/coding-tools-mcp
|
|
8
45
|
Project-URL: Documentation, https://github.com/xyTom/coding-tools-mcp/tree/main/docs
|
|
9
46
|
Project-URL: Source, https://github.com/xyTom/coding-tools-mcp
|
|
@@ -18,7 +55,6 @@ Requires-Dist: ruff<0.16,>=0.15; extra == "dev"
|
|
|
18
55
|
Requires-Dist: typing_extensions>=4.12; extra == "dev"
|
|
19
56
|
Provides-Extra: image
|
|
20
57
|
Requires-Dist: Pillow>=10.0; extra == "image"
|
|
21
|
-
Dynamic: license-file
|
|
22
58
|
|
|
23
59
|
# Coding Tools MCP
|
|
24
60
|
|
|
@@ -184,7 +220,7 @@ scripts/tunnel.sh ngrok /path/to/repo
|
|
|
184
220
|
scripts/tunnel.sh devtunnel /path/to/repo
|
|
185
221
|
```
|
|
186
222
|
|
|
187
|
-
For clients that support custom headers, use bearer-token auth with `Authorization: Bearer <token>`. For MCP clients that speak OAuth 2.1 Authorization Code + PKCE, use `CODING_TOOLS_MCP_AUTH_MODE=oauth` with `scripts/tunnel.sh` (or `scripts/install.sh --auth-mode oauth`)
|
|
223
|
+
For clients that support custom headers, use bearer-token auth with `Authorization: Bearer <token>`. For MCP clients that speak OAuth 2.1 Authorization Code + PKCE, use `CODING_TOOLS_MCP_AUTH_MODE=oauth` with `scripts/tunnel.sh` (or `scripts/install.sh --auth-mode oauth`). The server can infer its OAuth issuer from the tunnel request URL, so one-shot tunnels like cloudflared work without setting `CODING_TOOLS_MCP_SERVER_URL` before startup; set it only when you want to pin a stable issuer. The script prints a generated OAuth password, accepts any non-empty client_id by default, and lets you opt into `CODING_TOOLS_MCP_OAUTH_CLIENT_ID`/`CODING_TOOLS_MCP_OAUTH_CLIENT_SECRET` only when you need to lock down a confidential client. Clients that cannot send custom bearer headers and do not speak OAuth should use anonymous `read-only` mode only for local/testing tunnels, or be placed behind an external auth proxy for production use.
|
|
188
224
|
|
|
189
225
|
See [docs/remote-mcp.md](docs/remote-mcp.md) for the exact modes and security notes.
|
|
190
226
|
|
|
@@ -10,5 +10,4 @@ coding_tools_mcp.egg-info/SOURCES.txt
|
|
|
10
10
|
coding_tools_mcp.egg-info/dependency_links.txt
|
|
11
11
|
coding_tools_mcp.egg-info/entry_points.txt
|
|
12
12
|
coding_tools_mcp.egg-info/requires.txt
|
|
13
|
-
coding_tools_mcp.egg-info/top_level.txt
|
|
14
|
-
docs/profile-v0.1.md
|
|
13
|
+
coding_tools_mcp.egg-info/top_level.txt
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=77"]
|
|
2
|
+
requires = ["setuptools>=68,<77"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "coding-tools-mcp"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6"
|
|
8
8
|
description = "Workspace-confined coding tools exposed as an MCP server."
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"PyJWT>=2.8",
|
|
12
12
|
]
|
|
13
13
|
readme = "README.md"
|
|
14
|
-
license = "
|
|
14
|
+
license = { file = "LICENSE" }
|
|
15
15
|
authors = [{ name = "Coding Tools MCP Contributors" }]
|
|
16
16
|
|
|
17
17
|
[project.urls]
|