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.
@@ -1,9 +1,46 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.2
2
2
  Name: coding-tools-mcp
3
- Version: 0.1.5
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-Expression: LicenseRef-Coding-Tools-MCP-Source-Available
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`); the only env var you need to set is `CODING_TOOLS_MCP_SERVER_URL` (which must match the tunnel's stable public URL), and the script prints generated `CLIENT_ID`/`CLIENT_SECRET`/`PASSWORD` values on startup. 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.
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`); the only env var you need to set is `CODING_TOOLS_MCP_SERVER_URL` (which must match the tunnel's stable public URL), and the script prints generated `CLIENT_ID`/`CLIENT_SECRET`/`PASSWORD` values on startup. 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.
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
 
@@ -1,3 +1,3 @@
1
1
  """Coding Tools MCP server package."""
2
2
 
3
- __version__ = "0.1.5"
3
+ __version__ = "0.1.6"
@@ -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": cfg.server_url, "aud": cfg.server_url, "iat": now, "exp": now + cfg.token_ttl, "scope": "mcp"},
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=cfg.server_url, issuer=cfg.server_url)
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.runtime.oauth_config.server_url.rstrip("/")
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 = cfg.server_url.rstrip("/")
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": ["client_secret_post", "client_secret_basic"],
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 = cfg.server_url.rstrip("/")
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 secrets.compare_digest(client_id, cfg.client_id):
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 secrets.compare_digest(client_id, cfg.client_id):
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
- self.send_json({"error": "invalid_request", "error_description": "Content-Type must be application/x-www-form-urlencoded"}, status=400)
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 secrets.compare_digest(client_id, cfg.client_id):
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
- access_token = _create_oauth_token(cfg)
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
- password = os.environ.get(f"{ENV_PREFIX}_OAUTH_PASSWORD") or ""
4466
- server_url = os.environ.get(f"{ENV_PREFIX}_SERVER_URL") or ""
4467
- if not all([client_id, client_secret, password, server_url]):
4468
- print(
4469
- "ERROR: --oauth-mode requires CODING_TOOLS_MCP_OAUTH_CLIENT_ID, "
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.rstrip("/"),
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("WARNING: --auth-token is ignored when --oauth-mode is active.", file=sys.stderr)
4501
- auth_token = None
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
- auth_label = f"oauth2 enabled (server_url={oauth_config.server_url})"
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"requires env vars {ENV_PREFIX}_OAUTH_CLIENT_ID, {ENV_PREFIX}_OAUTH_CLIENT_SECRET, "
4645
- f"{ENV_PREFIX}_OAUTH_PASSWORD, {ENV_PREFIX}_SERVER_URL"
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.4
1
+ Metadata-Version: 2.2
2
2
  Name: coding-tools-mcp
3
- Version: 0.1.5
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-Expression: LicenseRef-Coding-Tools-MCP-Source-Available
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`); the only env var you need to set is `CODING_TOOLS_MCP_SERVER_URL` (which must match the tunnel's stable public URL), and the script prints generated `CLIENT_ID`/`CLIENT_SECRET`/`PASSWORD` values on startup. 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.
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.5"
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 = "LicenseRef-Coding-Tools-MCP-Source-Available"
14
+ license = { file = "LICENSE" }
15
15
  authors = [{ name = "Coding Tools MCP Contributors" }]
16
16
 
17
17
  [project.urls]