auto-coder-web 0.1.60__py3-none-any.whl → 0.1.62__py3-none-any.whl

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 (111) hide show
  1. auto_coder_web/common_router/auto_coder_conf_router.py +3 -3
  2. auto_coder_web/common_router/completions_router.py +15 -8
  3. auto_coder_web/common_router/file_router.py +39 -14
  4. auto_coder_web/expert_routers/history_router.py +1 -1
  5. auto_coder_web/file_manager.py +178 -3
  6. auto_coder_web/proxy.py +3 -1
  7. auto_coder_web/routers/auto_router.py +1 -1
  8. auto_coder_web/routers/chat_router.py +2 -2
  9. auto_coder_web/routers/coding_router.py +3 -3
  10. auto_coder_web/routers/commit_router.py +1 -1
  11. auto_coder_web/routers/config_router.py +9 -4
  12. auto_coder_web/routers/editable_preview_router.py +202 -0
  13. auto_coder_web/routers/mcp_router.py +287 -62
  14. auto_coder_web/version.py +1 -1
  15. auto_coder_web/web/HistoryPanel-WXEMtQ1V.js +1 -0
  16. auto_coder_web/web/bridge.js +189 -0
  17. auto_coder_web/web/{assets/cssMode-C28WkqEl.js → cssMode-CQYz0o1d.js} +1 -1
  18. auto_coder_web/web/{assets/freemarker2-ZqPV3OAn.js → freemarker2-BJU3cSen.js} +1 -1
  19. auto_coder_web/web/{assets/handlebars-BeVH6MoZ.js → handlebars-VxUdciXQ.js} +1 -1
  20. auto_coder_web/web/{assets/html-CbnuufYM.js → html-DEgskwXL.js} +1 -1
  21. auto_coder_web/web/{assets/htmlMode-CpsZVLwy.js → htmlMode-58cXxJlI.js} +1 -1
  22. auto_coder_web/web/index.html +2 -2
  23. auto_coder_web/web/{assets/javascript-CUA7SFHC.js → javascript-DbZlm-ig.js} +1 -1
  24. auto_coder_web/web/{assets/jsonMode-BzAAFwlN.js → jsonMode-C2HKgmE0.js} +1 -1
  25. auto_coder_web/web/{assets/liquid-B1UkAnrE.js → liquid-BFft-XkQ.js} +1 -1
  26. auto_coder_web/web/main-DxnFm18B.css +32 -0
  27. auto_coder_web/web/{assets/index-U694gZ5i.js → main.js} +372 -359
  28. auto_coder_web/web/{assets/mdx-2eOqd4a8.js → mdx-DZdDhrJW.js} +1 -1
  29. auto_coder_web/web/{assets/python-DadwP7sa.js → python-DOHUGzLU.js} +1 -1
  30. auto_coder_web/web/{assets/razor-CCybM3Rs.js → razor-Dh5mSHi2.js} +1 -1
  31. auto_coder_web/web/{assets/tsMode-Bk47zhhf.js → tsMode-tyI6CeIR.js} +1 -1
  32. auto_coder_web/web/{assets/typescript-vvCv9Db5.js → typescript-DZzM_VgT.js} +1 -1
  33. auto_coder_web/web/{assets/xml-CLM5-Fko.js → xml-Dl6413Na.js} +1 -1
  34. auto_coder_web/web/{assets/yaml-BF217FNY.js → yaml-DML583wh.js} +1 -1
  35. {auto_coder_web-0.1.60.dist-info → auto_coder_web-0.1.62.dist-info}/METADATA +2 -2
  36. {auto_coder_web-0.1.60.dist-info → auto_coder_web-0.1.62.dist-info}/RECORD +110 -107
  37. auto_coder_web/web/assets/index-Bnnll9cx.css +0 -32
  38. /auto_coder_web/web/{assets/abap-BrgZPUOV.js → abap-BrgZPUOV.js} +0 -0
  39. /auto_coder_web/web/{assets/apex-DyP6w7ZV.js → apex-DyP6w7ZV.js} +0 -0
  40. /auto_coder_web/web/{assets/azcli-BaLxmfj-.js → azcli-BaLxmfj-.js} +0 -0
  41. /auto_coder_web/web/{assets/bat-CFOPXBzS.js → bat-CFOPXBzS.js} +0 -0
  42. /auto_coder_web/web/{assets/bicep-BfEKNvv3.js → bicep-BfEKNvv3.js} +0 -0
  43. /auto_coder_web/web/{assets/cameligo-BFG1Mk7z.js → cameligo-BFG1Mk7z.js} +0 -0
  44. /auto_coder_web/web/{assets/clojure-DTECt2xU.js → clojure-DTECt2xU.js} +0 -0
  45. /auto_coder_web/web/{assets/codicon-DCmgc-ay.ttf → codicon-DCmgc-ay.ttf} +0 -0
  46. /auto_coder_web/web/{assets/coffee-CDGzqUPQ.js → coffee-CDGzqUPQ.js} +0 -0
  47. /auto_coder_web/web/{assets/cpp-CLLBncYj.js → cpp-CLLBncYj.js} +0 -0
  48. /auto_coder_web/web/{assets/csharp-dUCx_-0o.js → csharp-dUCx_-0o.js} +0 -0
  49. /auto_coder_web/web/{assets/csp-5Rap-vPy.js → csp-5Rap-vPy.js} +0 -0
  50. /auto_coder_web/web/{assets/css-D3h14YRZ.js → css-D3h14YRZ.js} +0 -0
  51. /auto_coder_web/web/{assets/cypher-DrQuvNYM.js → cypher-DrQuvNYM.js} +0 -0
  52. /auto_coder_web/web/{assets/dart-CFKIUWau.js → dart-CFKIUWau.js} +0 -0
  53. /auto_coder_web/web/{assets/dockerfile-Zznr-cwX.js → dockerfile-Zznr-cwX.js} +0 -0
  54. /auto_coder_web/web/{assets/ecl-Ce3n6wWz.js → ecl-Ce3n6wWz.js} +0 -0
  55. /auto_coder_web/web/{assets/elixir-deUWdS0T.js → elixir-deUWdS0T.js} +0 -0
  56. /auto_coder_web/web/{assets/flow9-i9-g7ZhI.js → flow9-i9-g7ZhI.js} +0 -0
  57. /auto_coder_web/web/{assets/fsharp-CzKuDChf.js → fsharp-CzKuDChf.js} +0 -0
  58. /auto_coder_web/web/{assets/go-Cphgjts3.js → go-Cphgjts3.js} +0 -0
  59. /auto_coder_web/web/{assets/graphql-Cg7bfA9N.js → graphql-Cg7bfA9N.js} +0 -0
  60. /auto_coder_web/web/{assets/hcl-0cvrggvQ.js → hcl-0cvrggvQ.js} +0 -0
  61. /auto_coder_web/web/{assets/ini-Drc7WvVn.js → ini-Drc7WvVn.js} +0 -0
  62. /auto_coder_web/web/{assets/java-B_fMsGYe.js → java-B_fMsGYe.js} +0 -0
  63. /auto_coder_web/web/{assets/julia-Bqgm2twL.js → julia-Bqgm2twL.js} +0 -0
  64. /auto_coder_web/web/{assets/kotlin-BSkB5QuD.js → kotlin-BSkB5QuD.js} +0 -0
  65. /auto_coder_web/web/{assets/less-BsTHnhdd.js → less-BsTHnhdd.js} +0 -0
  66. /auto_coder_web/web/{assets/lexon-YWi4-JPR.js → lexon-YWi4-JPR.js} +0 -0
  67. /auto_coder_web/web/{assets/lua-nf6ki56Z.js → lua-nf6ki56Z.js} +0 -0
  68. /auto_coder_web/web/{assets/m3-Cpb6xl2v.js → m3-Cpb6xl2v.js} +0 -0
  69. /auto_coder_web/web/{assets/markdown-DSZPf7rp.js → markdown-DSZPf7rp.js} +0 -0
  70. /auto_coder_web/web/{assets/mips-B_c3zf-v.js → mips-B_c3zf-v.js} +0 -0
  71. /auto_coder_web/web/{assets/msdax-rUNN04Wq.js → msdax-rUNN04Wq.js} +0 -0
  72. /auto_coder_web/web/{assets/mysql-DDwshQtU.js → mysql-DDwshQtU.js} +0 -0
  73. /auto_coder_web/web/{assets/objective-c-B5zXfXm9.js → objective-c-B5zXfXm9.js} +0 -0
  74. /auto_coder_web/web/{assets/pascal-CXOwvkN_.js → pascal-CXOwvkN_.js} +0 -0
  75. /auto_coder_web/web/{assets/pascaligo-Bc-ZgV77.js → pascaligo-Bc-ZgV77.js} +0 -0
  76. /auto_coder_web/web/{assets/perl-CwNk8-XU.js → perl-CwNk8-XU.js} +0 -0
  77. /auto_coder_web/web/{assets/pgsql-tGk8EFnU.js → pgsql-tGk8EFnU.js} +0 -0
  78. /auto_coder_web/web/{assets/php-CpIb_Oan.js → php-CpIb_Oan.js} +0 -0
  79. /auto_coder_web/web/{assets/pla-B03wrqEc.js → pla-B03wrqEc.js} +0 -0
  80. /auto_coder_web/web/{assets/postiats-BKlk5iyT.js → postiats-BKlk5iyT.js} +0 -0
  81. /auto_coder_web/web/{assets/powerquery-Bhzvs7bI.js → powerquery-Bhzvs7bI.js} +0 -0
  82. /auto_coder_web/web/{assets/powershell-Dd3NCNK9.js → powershell-Dd3NCNK9.js} +0 -0
  83. /auto_coder_web/web/{assets/protobuf-COyEY5Pt.js → protobuf-COyEY5Pt.js} +0 -0
  84. /auto_coder_web/web/{assets/pug-BaJupSGV.js → pug-BaJupSGV.js} +0 -0
  85. /auto_coder_web/web/{assets/qsharp-DXyYeYxl.js → qsharp-DXyYeYxl.js} +0 -0
  86. /auto_coder_web/web/{assets/r-CdQndTaG.js → r-CdQndTaG.js} +0 -0
  87. /auto_coder_web/web/{assets/redis-CVwtpugi.js → redis-CVwtpugi.js} +0 -0
  88. /auto_coder_web/web/{assets/redshift-25W9uPmb.js → redshift-25W9uPmb.js} +0 -0
  89. /auto_coder_web/web/{assets/restructuredtext-DfzH4Xui.js → restructuredtext-DfzH4Xui.js} +0 -0
  90. /auto_coder_web/web/{assets/ruby-Cp1zYvxS.js → ruby-Cp1zYvxS.js} +0 -0
  91. /auto_coder_web/web/{assets/rust-D5C2fndG.js → rust-D5C2fndG.js} +0 -0
  92. /auto_coder_web/web/{assets/sb-CDntyWJ8.js → sb-CDntyWJ8.js} +0 -0
  93. /auto_coder_web/web/{assets/scala-BoFRg7Ot.js → scala-BoFRg7Ot.js} +0 -0
  94. /auto_coder_web/web/{assets/scheme-Bio4gycK.js → scheme-Bio4gycK.js} +0 -0
  95. /auto_coder_web/web/{assets/scss-4Ik7cdeQ.js → scss-4Ik7cdeQ.js} +0 -0
  96. /auto_coder_web/web/{assets/shell-CX-rkNHf.js → shell-CX-rkNHf.js} +0 -0
  97. /auto_coder_web/web/{assets/solidity-Tw7wswEv.js → solidity-Tw7wswEv.js} +0 -0
  98. /auto_coder_web/web/{assets/sophia-C5WLch3f.js → sophia-C5WLch3f.js} +0 -0
  99. /auto_coder_web/web/{assets/sparql-DHaeiCBh.js → sparql-DHaeiCBh.js} +0 -0
  100. /auto_coder_web/web/{assets/sql-CCSDG5nI.js → sql-CCSDG5nI.js} +0 -0
  101. /auto_coder_web/web/{assets/st-pnP8ivHi.js → st-pnP8ivHi.js} +0 -0
  102. /auto_coder_web/web/{assets/swift-DwJ7jVG9.js → swift-DwJ7jVG9.js} +0 -0
  103. /auto_coder_web/web/{assets/systemverilog-B9Xyijhd.js → systemverilog-B9Xyijhd.js} +0 -0
  104. /auto_coder_web/web/{assets/tcl-DnHyzjbg.js → tcl-DnHyzjbg.js} +0 -0
  105. /auto_coder_web/web/{assets/twig-CPajHgWi.js → twig-CPajHgWi.js} +0 -0
  106. /auto_coder_web/web/{assets/typespec-D-MeaMDU.js → typespec-D-MeaMDU.js} +0 -0
  107. /auto_coder_web/web/{assets/vb-DgyLZaXg.js → vb-DgyLZaXg.js} +0 -0
  108. /auto_coder_web/web/{assets/wgsl-BIv9DU6q.js → wgsl-BIv9DU6q.js} +0 -0
  109. {auto_coder_web-0.1.60.dist-info → auto_coder_web-0.1.62.dist-info}/WHEEL +0 -0
  110. {auto_coder_web-0.1.60.dist-info → auto_coder_web-0.1.62.dist-info}/entry_points.txt +0 -0
  111. {auto_coder_web-0.1.60.dist-info → auto_coder_web-0.1.62.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,202 @@
1
+ import logging
2
+ from pydantic import BaseModel
3
+ import requests
4
+ from fastapi import APIRouter, HTTPException, Request, Query, Body
5
+ from fastapi.responses import HTMLResponse
6
+ from bs4 import BeautifulSoup
7
+ from urllib.parse import urljoin, urlparse
8
+ import aiofiles
9
+ from pathlib import Path
10
+ import json
11
+ from datetime import datetime
12
+ import uuid
13
+ from loguru import logger as global_logger
14
+
15
+ logger = global_logger.bind(name="editable_preview")
16
+ router = APIRouter()
17
+
18
+ # --- Configuration ---
19
+ BRIDGE_SCRIPT_PATH = "/bridge.js" # Path where the frontend serves bridge.js
20
+ SAVED_PREVIEWS_DIR = Path(".auto-coder/auto-coder.web/editable-previews")
21
+ SAVED_PREVIEWS_DIR.mkdir(parents=True, exist_ok=True)
22
+ # --- End Configuration ---
23
+
24
+
25
+ def resolve_url(base_url: str, relative_url: str) -> str:
26
+ """Resolves a relative URL against a base URL."""
27
+ try:
28
+ return urljoin(base_url, relative_url)
29
+ except ValueError:
30
+ # Handle cases where relative_url might be invalid
31
+ return relative_url
32
+
33
+ def rewrite_html_resources(soup: BeautifulSoup, base_url: str, proxy_base: str):
34
+ """Rewrites relative URLs in common HTML tags to absolute URLs or proxy URLs."""
35
+ tags_attributes = {
36
+ 'a': 'href',
37
+ 'link': 'href',
38
+ 'script': 'src',
39
+ 'img': 'src',
40
+ 'iframe': 'src',
41
+ 'form': 'action',
42
+ # Add other tags/attributes as needed
43
+ }
44
+
45
+ for tag_name, attr_name in tags_attributes.items():
46
+ for tag in soup.find_all(tag_name):
47
+ attr_value = tag.get(attr_name)
48
+ if attr_value:
49
+ # Resolve relative URLs relative to the original page's base URL
50
+ absolute_url = resolve_url(base_url, attr_value)
51
+
52
+ # Optionally, rewrite to go through proxy (more complex, might break things)
53
+ # For simplicity now, just make them absolute to original domain
54
+ # proxy_url = f"{proxy_base}?url={absolute_url}" # Example proxy rewrite
55
+ # tag[attr_name] = proxy_url
56
+ tag[attr_name] = absolute_url # Keep original absolute for now
57
+
58
+ # Special handling for inline styles with url()
59
+ for tag in soup.find_all(style=True):
60
+ # Basic url() rewriting - might need a more robust CSS parser for complex cases
61
+ style_content = tag['style']
62
+ # This regex is basic, might need refinement
63
+ import re
64
+ def replace_url(match):
65
+ url = match.group(1).strip("'\"")
66
+ absolute_url = resolve_url(base_url, url)
67
+ return f"url('{absolute_url}')"
68
+
69
+ tag['style'] = re.sub(r"url\((.*?)\)", replace_url, style_content)
70
+
71
+ # Consider rewriting srcset for images as well if needed
72
+
73
+ @router.get("/api/editable-preview/proxy", response_class=HTMLResponse)
74
+ async def proxy_external_url(request: Request, url: str = Query(...)):
75
+ """
76
+ Fetches an external URL, injects a bridge script, and returns the HTML.
77
+ Handles basic resource URL rewriting.
78
+ """
79
+ if not url or not url.startswith(('http://', 'https://')):
80
+ raise HTTPException(status_code=400, detail="Invalid or missing URL parameter")
81
+
82
+ try:
83
+ headers = {
84
+ # Mimic a browser request
85
+ 'User-Agent': request.headers.get('user-agent', 'Mozilla/5.0'),
86
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
87
+ 'Accept-Language': 'en-US,en;q=0.5',
88
+ 'Referer': urlparse(url).scheme + "://" + urlparse(url).netloc # Set referer to target domain
89
+ }
90
+ response = requests.get(url, headers=headers, timeout=15, allow_redirects=True)
91
+ response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
92
+
93
+ # Check content type
94
+ content_type = response.headers.get('content-type', '').lower()
95
+ if 'text/html' not in content_type:
96
+ logger.warning(f"Proxied URL {url} returned non-HTML content-type: {content_type}")
97
+ # Return as is if not HTML, or raise error? Decide based on desired behavior.
98
+ # For now, let's try parsing anyway, but log a warning.
99
+ # raise HTTPException(status_code=400, detail=f"URL did not return HTML content. Content-Type: {content_type}")
100
+
101
+ # Parse HTML content
102
+ soup = BeautifulSoup(response.text, 'html.parser')
103
+
104
+ # --- Inject Bridge Script ---
105
+ bridge_script_tag = soup.new_tag("script")
106
+ bridge_script_tag["src"] = BRIDGE_SCRIPT_PATH
107
+ bridge_script_tag["defer"] = True # Load after HTML parsing but before DOMContentLoaded
108
+
109
+ # Try injecting into <head>, fallback to <body>
110
+ head = soup.find('head')
111
+ if head:
112
+ head.append(bridge_script_tag)
113
+ else:
114
+ body = soup.find('body')
115
+ if body:
116
+ body.append(bridge_script_tag)
117
+ else:
118
+ # If no head or body, append to the root (less ideal)
119
+ soup.append(bridge_script_tag)
120
+ # --- End Injection ---
121
+
122
+ # --- Rewrite Resource URLs ---
123
+ # Get the final URL after potential redirects
124
+ final_url = response.url
125
+ # Define the base for our proxy endpoint (needed if we decide to proxy resources)
126
+ proxy_base_url = str(request.base_url) + "api/editable-preview/proxy"
127
+ rewrite_html_resources(soup, final_url, proxy_base_url)
128
+ # --- End Rewriting ---
129
+
130
+ # Return modified HTML
131
+ modified_html = str(soup)
132
+
133
+ # Prepare response, try to remove X-Frame-Options
134
+ resp_headers = dict(response.headers)
135
+ # Remove security headers that prevent framing (use with caution)
136
+ resp_headers.pop('X-Frame-Options', None)
137
+ resp_headers.pop('Content-Security-Policy', None)
138
+ # Remove Content-Length header
139
+ resp_headers.pop('Content-Length', None)
140
+ # Ensure correct content type
141
+ resp_headers['Content-Type'] = 'text/html; charset=utf-8' # Force UTF-8
142
+
143
+ return HTMLResponse(content=modified_html, headers=resp_headers)
144
+
145
+ except requests.exceptions.Timeout:
146
+ raise HTTPException(status_code=504, detail=f"Timeout while fetching URL: {url}")
147
+ except requests.exceptions.RequestException as e:
148
+ logger.error(f"Error fetching URL {url}: {e}")
149
+ raise HTTPException(status_code=502, detail=f"Failed to fetch URL: {url}. Error: {str(e)}")
150
+ except Exception as e:
151
+ logger.error(f"Error processing proxy request for {url}: {e}", exc_info=True)
152
+ raise HTTPException(status_code=500, detail=f"Internal server error processing URL: {url}")
153
+
154
+
155
+ class SavePreviewRequest(BaseModel):
156
+ url: str
157
+ html_content: str
158
+
159
+ @router.post("/api/editable-preview/save")
160
+ async def save_edited_preview(payload: SavePreviewRequest = Body(...)):
161
+ """Saves the edited HTML content for a given URL."""
162
+ try:
163
+ url = payload.url
164
+ html_content = payload.html_content
165
+
166
+ if not url or not html_content:
167
+ raise HTTPException(status_code=400, detail="Missing URL or HTML content")
168
+
169
+ # Generate a unique filename or use a hash of the URL
170
+ parsed_url = urlparse(url)
171
+ # Sanitize filename (basic example)
172
+ filename_base = f"{parsed_url.netloc}_{parsed_url.path}".replace('/', '_').replace('.', '_')
173
+ filename_base = "".join(c for c in filename_base if c.isalnum() or c in ('_', '-')).rstrip('_')[:100] # Limit length
174
+
175
+ save_id = str(uuid.uuid4())
176
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
177
+ filename = f"{filename_base}_{timestamp}_{save_id}.html"
178
+ filepath = SAVED_PREVIEWS_DIR / filename
179
+
180
+ metadata = {
181
+ "original_url": url,
182
+ "saved_at": datetime.now().isoformat(),
183
+ "save_id": save_id,
184
+ "filename": filename
185
+ }
186
+ meta_filepath = SAVED_PREVIEWS_DIR / f"{filename}.meta.json"
187
+
188
+
189
+ async with aiofiles.open(filepath, mode='w', encoding='utf-8') as f:
190
+ await f.write(html_content)
191
+
192
+ async with aiofiles.open(meta_filepath, mode='w', encoding='utf-8') as f:
193
+ await f.write(json.dumps(metadata, indent=2))
194
+
195
+ logger.info(f"Saved edited preview for URL '{url}' to '{filepath}'")
196
+ return {"status": "success", "save_id": save_id, "filename": filename}
197
+
198
+ except HTTPException:
199
+ raise # Re-raise HTTP exceptions
200
+ except Exception as e:
201
+ logger.error(f"Error saving edited preview for URL '{payload.url}': {e}", exc_info=True)
202
+ raise HTTPException(status_code=500, detail="Failed to save edited preview")
@@ -4,94 +4,277 @@ import os
4
4
  from fastapi import APIRouter, HTTPException, Request, Depends
5
5
  from pydantic import BaseModel, Field
6
6
  from typing import Dict, Any, Optional, List
7
- from autocoder.common.mcp_server import (
8
- get_mcp_server,
9
- McpInstallRequest,
10
- McpRemoveRequest,
11
- McpListRequest,
12
- McpListRunningRequest,
13
- McpRefreshRequest,
7
+ from autocoder.common.mcp_server_types import (
8
+ McpInstallRequest,
9
+ McpRemoveRequest,
10
+ McpListRequest,
11
+ McpListRunningRequest,
12
+ McpRefreshRequest,
14
13
  McpServerInfoRequest,
15
- McpResponse
14
+ McpResponse,
15
+ InstallResult,
16
+ RemoveResult,
17
+ ListResult,
18
+ ListRunningResult,
19
+ RefreshResult,
20
+ QueryResult,
21
+ ErrorResult,
22
+ ServerInfo,
23
+ ExternalServerInfo,
24
+ ServerConfig, # Added for InstallResult
25
+ MarketplaceAddRequest,
26
+ MarketplaceAddResult,
27
+ MarketplaceUpdateRequest, # Added for update endpoint
28
+ MarketplaceUpdateResult, # Added for update endpoint
16
29
  )
17
- from autocoder.common.printer import Printer # For messages
18
- from autocoder.chat_auto_coder_lang import get_message_with_format # For formatted messages
30
+ from autocoder.common.mcp_server import get_mcp_server
31
+ from autocoder.common.printer import Printer # For messages
32
+ from autocoder.chat_auto_coder_lang import (
33
+ get_message_with_format,
34
+ get_message,
35
+ ) # For formatted messages
19
36
  from loguru import logger
20
- from byzerllm.utils.langutil import asyncfy_with_semaphore
21
-
22
- # Use asyncfy_with_semaphore to wrap the synchronous send_request method
23
- async_send_request = asyncfy_with_semaphore(get_mcp_server().send_request, max_workers=5)
24
37
 
25
38
  router = APIRouter()
26
- printer = Printer() # Initialize printer for messages
39
+ printer = Printer() # Initialize printer for messages
40
+
41
+
42
+ # Helper function to run the synchronous send_request in a thread
43
+ async def send_mcp_request_async(*args, **kwargs) -> McpResponse:
44
+ """Runs the synchronous MCP send_request in a separate thread."""
45
+ return await asyncio.to_thread(get_mcp_server().send_request, *args, **kwargs)
46
+
27
47
 
28
48
  # --- Pydantic Models for Requests ---
29
49
 
30
- class McpAddRequest(BaseModel):
31
- server_config: str = Field(..., description="Server configuration string (command-line style or JSON)")
50
+
51
+ class McpInstallRequestModel(BaseModel):
52
+ server_config: str = Field(
53
+ ..., description="Server configuration string (command-line style or JSON)"
54
+ )
55
+
56
+
57
+ # Model for the new /api/mcp/add endpoint
58
+ class MarketplaceAddRequestModel(BaseModel):
59
+ name: str = Field(
60
+ ..., description="Name of the MCP server to add to the marketplace"
61
+ )
62
+ description: Optional[str] = Field("", description="Description of the MCP server")
63
+ mcp_type: str = Field(
64
+ "command", description="Type of MCP server (e.g., 'command', 'sse')"
65
+ )
66
+ command: Optional[str] = Field(
67
+ None, description="Command to run the server (if type is 'command')"
68
+ ) # Allow None
69
+ args: Optional[List[str]] = Field(None, description="Arguments for the command")
70
+ env: Optional[Dict[str, str]] = Field(
71
+ None, description="Environment variables for the command"
72
+ )
73
+ url: Optional[str] = Field(
74
+ None, description="URL endpoint for the server (if type is 'sse')"
75
+ ) # Allow None
76
+
77
+
78
+ # Model for the /api/mcp/update endpoint
79
+ class MarketplaceUpdateRequestModel(BaseModel):
80
+ name: str = Field(
81
+ ..., description="Name of the MCP server to update (used as identifier)"
82
+ )
83
+ description: Optional[str] = Field(
84
+ None, description="Updated description of the MCP server"
85
+ )
86
+ mcp_type: Optional[str] = Field(
87
+ None, description="Updated type of MCP server"
88
+ ) # Allow None if not changing
89
+ command: Optional[str] = Field(None, description="Updated command")
90
+ args: Optional[List[str]] = Field(None, description="Updated arguments")
91
+ env: Optional[Dict[str, str]] = Field(
92
+ None, description="Updated environment variables (replaces existing)"
93
+ )
94
+ url: Optional[str] = Field(None, description="Updated URL endpoint")
95
+
32
96
 
33
97
  class McpRemoveRequestModel(BaseModel):
34
98
  server_name: str = Field(..., description="Name of the MCP server to remove")
35
99
 
100
+
36
101
  class McpRefreshRequestModel(BaseModel):
37
- server_name: Optional[str] = Field(None, description="Name of the MCP server to refresh (optional, refreshes all if None)")
102
+ server_name: Optional[str] = Field(
103
+ None,
104
+ description="Name of the MCP server to refresh (optional, refreshes all if None)",
105
+ )
106
+
38
107
 
39
108
  class McpInfoRequestModel(BaseModel):
40
109
  # Assuming model and product_mode might come from global config or request context later
41
110
  # For now, let's make them optional or derive them if possible
42
111
  model: Optional[str] = None
43
- product_mode: Optional[str] = None # Example: "lite", "pro"
112
+ product_mode: Optional[str] = None # Example: "lite", "pro"
113
+
44
114
 
45
115
  # --- Helper Function to Handle MCP Responses ---
46
116
 
47
- async def handle_mcp_response(request: Any, success_key: str, error_key: str, **kwargs) -> Dict[str, Any]:
117
+
118
+ async def handle_mcp_response(
119
+ request: Any, success_key: str, error_key: str, **kwargs
120
+ ) -> Dict[str, Any]:
48
121
  """Handles sending request to MCP server and formatting the response."""
49
122
  try:
50
- response: McpResponse = await async_send_request(request)
123
+ response: McpResponse = await send_mcp_request_async(request)
51
124
  if response.error:
52
125
  logger.error(f"MCP Error ({error_key}): {response.error}")
53
126
  # Use get_message_with_format if available, otherwise use the raw error
54
127
  error_message = response.error
55
128
  try:
56
129
  # Attempt to format the error message if a key is provided
57
- formatted_error = get_message_with_format(error_key, error=response.error)
58
- if formatted_error: # Check if formatting was successful
130
+ formatted_error = get_message_with_format(
131
+ error_key, error=response.error
132
+ )
133
+ if formatted_error: # Check if formatting was successful
59
134
  error_message = formatted_error
60
- except Exception: # Catch potential errors during formatting
61
- pass # Stick with the original error message
135
+ except Exception: # Catch potential errors during formatting
136
+ pass # Stick with the original error message
62
137
  raise HTTPException(status_code=400, detail=error_message)
63
138
  else:
64
139
  # Use get_message_with_format for success message if available
65
140
  success_message = response.result
66
141
  try:
67
- formatted_success = get_message_with_format(success_key, result=response.result, **kwargs)
68
- if formatted_success: # Check if formatting was successful
142
+ formatted_success = get_message_with_format(
143
+ success_key, result=response.result, **kwargs
144
+ )
145
+ if formatted_success: # Check if formatting was successful
69
146
  success_message = formatted_success
70
147
  except Exception:
71
- pass # Stick with the original result message
72
- return {"status": "success", "message": success_message, "data": response.result} # Include raw data too
148
+ pass # Stick with the original result message
149
+ # Return the formatted message and the raw Pydantic model result
150
+ return {
151
+ "status": "success",
152
+ "message": success_message,
153
+ "raw_result": response.raw_result,
154
+ }
73
155
  except HTTPException as http_exc:
74
- raise http_exc # Re-raise HTTPException
156
+ raise http_exc # Re-raise HTTPException
75
157
  except Exception as e:
76
158
  logger.error(f"Unexpected error during MCP request ({error_key}): {str(e)}")
77
159
  raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
78
160
 
161
+
79
162
  # --- API Endpoints ---
80
163
 
164
+
165
+ @router.post("/api/mcp/install")
166
+ async def install_mcp_server(request: McpInstallRequestModel):
167
+ """
168
+ Installs or updates an MCP server configuration based on name, JSON, or command-line args.
169
+ Handles built-in, external, and custom server installations.
170
+ """
171
+ # First, try to find the server in the marketplace list via McpListRequest
172
+ try:
173
+ list_request = McpListRequest()
174
+ list_response: McpResponse = await send_mcp_request_async(list_request)
175
+
176
+ marketplace_item = None
177
+ if list_response.raw_result and isinstance(
178
+ list_response.raw_result, ListResult
179
+ ):
180
+ # Combine all server lists for searching
181
+ all_servers = list_response.raw_result.marketplace_items
182
+ for item in all_servers:
183
+ if item.name == request.server_config:
184
+ marketplace_item = item
185
+ break
186
+
187
+ if marketplace_item:
188
+ # If found in any list, create install request with the item
189
+ mcp_request = McpInstallRequest(market_install_item=marketplace_item)
190
+ logger.info(
191
+ f"Found '{request.server_config}' in available server lists. Installing using item. {marketplace_item}"
192
+ )
193
+ else:
194
+ # If not found in any list, assume it's a direct config string or an unknown name
195
+ mcp_request = McpInstallRequest(server_name_or_config=request.server_config)
196
+ logger.info(
197
+ f"'{request.server_config}' not found in available server lists. Installing using name/config string."
198
+ )
199
+
200
+ # Proceed with installation using the determined request type
201
+ return await handle_mcp_response(
202
+ mcp_request,
203
+ success_key="mcp_install_success",
204
+ error_key="mcp_install_error",
205
+ result=request.server_config, # Pass original config for success message formatting
206
+ )
207
+
208
+ except HTTPException as http_exc:
209
+ # Re-raise HTTP exceptions from handle_mcp_response or list request
210
+ raise http_exc
211
+ except Exception as e:
212
+ logger.error(
213
+ f"Error during MCP install process for '{request.server_config}': {e}"
214
+ )
215
+ # Fallback to original behavior if list fails or other errors occur
216
+ logger.warning("Falling back to direct install request due to previous error.")
217
+ mcp_request = McpInstallRequest(server_name_or_config=request.server_config)
218
+ return await handle_mcp_response(
219
+ mcp_request,
220
+ success_key="mcp_install_success",
221
+ error_key="mcp_install_error",
222
+ result=request.server_config,
223
+ )
224
+
225
+
81
226
  @router.post("/api/mcp/add")
82
- async def add_mcp_server(request: McpAddRequest):
227
+ async def add_marketplace_server(request: MarketplaceAddRequestModel):
83
228
  """
84
- Adds or updates an MCP server configuration.
85
- Accepts command-line style args or a JSON string.
229
+ Adds a new MCP server configuration to the marketplace file.
86
230
  """
87
- mcp_request = McpInstallRequest(server_name_or_config=request.server_config)
231
+ # Convert API model to the internal McpHub model
232
+ mcp_request = MarketplaceAddRequest(
233
+ name=request.name,
234
+ description=request.description,
235
+ mcp_type=request.mcp_type,
236
+ command=request.command,
237
+ args=request.args,
238
+ env=request.env,
239
+ url=request.url,
240
+ )
88
241
  return await handle_mcp_response(
89
242
  mcp_request,
90
- success_key="mcp_install_success",
91
- error_key="mcp_install_error",
92
- result=request.server_config # Pass original config for success message formatting
243
+ success_key="marketplace_add_success",
244
+ error_key="marketplace_add_error",
245
+ name=request.name, # Pass name for message formatting
93
246
  )
94
247
 
248
+
249
+ @router.post("/api/mcp/update")
250
+ async def update_marketplace_server(request: MarketplaceUpdateRequestModel):
251
+ """
252
+ Updates an existing MCP server configuration in the marketplace file.
253
+ Uses the 'name' field to identify the server to update.
254
+ """
255
+ # Convert API model to the internal McpHub model for update
256
+ # Note: We assume MarketplaceUpdateRequest exists in mcp_server
257
+ # and handles partial updates based on provided fields.
258
+ # If a field is None in the request, it might mean "don't update this field"
259
+ # or "set this field to None/empty", depending on McpHub's implementation.
260
+ # Here, we pass all fields from the request model.
261
+ mcp_request = MarketplaceUpdateRequest(
262
+ name=request.name, # Identifier
263
+ description=request.description,
264
+ mcp_type=request.mcp_type,
265
+ command=request.command,
266
+ args=request.args,
267
+ env=request.env,
268
+ url=request.url,
269
+ )
270
+ return await handle_mcp_response(
271
+ mcp_request,
272
+ success_key="marketplace_update_success", # Define this message key
273
+ error_key="marketplace_update_error", # Define this message key
274
+ name=request.name, # Pass name for message formatting
275
+ )
276
+
277
+
95
278
  @router.post("/api/mcp/remove")
96
279
  async def remove_mcp_server(request: McpRemoveRequestModel):
97
280
  """Removes an MCP server configuration by name."""
@@ -100,24 +283,36 @@ async def remove_mcp_server(request: McpRemoveRequestModel):
100
283
  mcp_request,
101
284
  success_key="mcp_remove_success",
102
285
  error_key="mcp_remove_error",
103
- result=request.server_name # Pass server name for success message formatting
286
+ result=request.server_name, # Pass server name for success message formatting
104
287
  )
105
288
 
289
+
106
290
  @router.get("/api/mcp/list")
107
291
  async def list_mcp_servers():
108
292
  """Lists all available built-in and external MCP servers."""
109
293
  mcp_request = McpListRequest()
110
294
  # Specific handling for list as the result is the data itself
111
295
  try:
112
- response: McpResponse = await async_send_request(mcp_request)
296
+ response: McpResponse = await send_mcp_request_async(mcp_request)
113
297
  if response.error:
114
298
  logger.error(f"MCP Error (mcp_list_builtin_error): {response.error}")
115
- error_message = get_message_with_format("mcp_list_builtin_error", error=response.error) or response.error
116
- raise HTTPException(status_code=400, detail=error_message)
299
+ error_message = (
300
+ get_message_with_format("mcp_list_builtin_error", error=response.error)
301
+ or response.error
302
+ )
303
+ # Ensure raw_result is included in the error detail if it's an ErrorResult
304
+ detail = error_message
305
+ if isinstance(response.raw_result, ErrorResult):
306
+ detail = f"{error_message} (Details: {response.raw_result.error})"
307
+ raise HTTPException(status_code=400, detail=detail)
117
308
  else:
118
- # Split the result string into a list for better JSON representation
119
- server_list = response.result.strip().split('\n') if response.result else []
120
- return {"status": "success", "servers": server_list}
309
+ # Return the raw_result which should be of type ListResult
310
+ # Ensure the response is structured consistently
311
+ return {
312
+ "status": "success",
313
+ "message": "MCP servers listed successfully.",
314
+ "raw_result": response.raw_result,
315
+ }
121
316
  except HTTPException as http_exc:
122
317
  raise http_exc
123
318
  except Exception as e:
@@ -131,58 +326,88 @@ async def list_running_mcp_servers():
131
326
  mcp_request = McpListRunningRequest()
132
327
  # Specific handling for list_running
133
328
  try:
134
- response: McpResponse = await async_send_request(mcp_request)
329
+ response: McpResponse = await send_mcp_request_async(mcp_request)
135
330
  if response.error:
136
331
  logger.error(f"MCP Error (mcp_list_running_error): {response.error}")
137
- error_message = get_message_with_format("mcp_list_running_error", error=response.error) or response.error
138
- raise HTTPException(status_code=400, detail=error_message)
332
+ error_message = (
333
+ get_message_with_format("mcp_list_running_error", error=response.error)
334
+ or response.error
335
+ )
336
+ # Ensure raw_result is included in the error detail if it's an ErrorResult
337
+ detail = error_message
338
+ if isinstance(response.raw_result, ErrorResult):
339
+ detail = f"{error_message} (Details: {response.raw_result.error})"
340
+ raise HTTPException(status_code=400, detail=detail)
139
341
  else:
140
- # Split the result string into a list
141
- running_server_list = response.result.strip().split('\n') if response.result else []
142
- # Clean up list (remove potential leading hyphens/spaces)
143
- cleaned_list = [s.strip().lstrip('-').strip() for s in running_server_list if s.strip()]
144
- return {"status": "success", "running_servers": cleaned_list}
342
+ # Return the raw_result which should be of type ListRunningResult
343
+ # Ensure the response is structured consistently
344
+ return {
345
+ "status": "success",
346
+ "message": "Running MCP servers listed successfully.",
347
+ "raw_result": response.raw_result,
348
+ }
145
349
  except HTTPException as http_exc:
146
350
  raise http_exc
147
351
  except Exception as e:
148
352
  logger.error(f"Unexpected error during MCP list_running request: {str(e)}")
149
353
  raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
150
354
 
355
+
151
356
  @router.post("/api/mcp/refresh")
152
357
  async def refresh_mcp_connections(request: McpRefreshRequestModel):
153
358
  """Refreshes connections to MCP servers (all or a specific one)."""
154
359
  mcp_request = McpRefreshRequest(name=request.server_name)
155
360
  return await handle_mcp_response(
156
- mcp_request,
157
- success_key="mcp_refresh_success",
158
- error_key="mcp_refresh_error"
361
+ mcp_request, success_key="mcp_refresh_success", error_key="mcp_refresh_error"
159
362
  )
160
363
 
364
+
161
365
  @router.get("/api/mcp/info")
162
- async def get_mcp_server_info(model: Optional[str] = None, product_mode: Optional[str] = None):
366
+ async def get_mcp_server_info(
367
+ model: Optional[str] = None, product_mode: Optional[str] = "lite"
368
+ ):
163
369
  """Gets detailed information about connected MCP servers."""
164
370
  # TODO: Determine how to get model/product_mode - from app state, global config, or request?
165
371
  # Using optional query params for now.
166
372
  mcp_request = McpServerInfoRequest(model=model, product_mode=product_mode)
167
373
  # Specific handling for info
168
374
  try:
169
- response: McpResponse = await async_send_request(mcp_request)
375
+ response: McpResponse = await send_mcp_request_async(mcp_request)
170
376
  if response.error:
171
377
  logger.error(f"MCP Error (mcp_server_info_error): {response.error}")
172
- error_message = get_message_with_format("mcp_server_info_error", error=response.error) or response.error
173
- raise HTTPException(status_code=400, detail=error_message)
378
+ error_message = (
379
+ get_message_with_format("mcp_server_info_error", error=response.error)
380
+ or response.error
381
+ )
382
+ # Ensure raw_result is included in the error detail if it's an ErrorResult
383
+ detail = error_message
384
+ if isinstance(response.raw_result, ErrorResult):
385
+ detail = f"{error_message} (Details: {response.raw_result.error})"
386
+ raise HTTPException(status_code=400, detail=detail)
174
387
  else:
175
- # The result is likely a markdown string or complex structure. Return as is.
176
- return {"status": "success", "info": response.result}
388
+ # Return the raw_result. It might be a string or a specific Pydantic model later.
389
+ # For now, we assume it's included in McpResponse.raw_result
390
+ # Ensure the response is structured consistently
391
+ # The success message might vary or be generic
392
+ success_message = (
393
+ get_message_with_format("mcp_server_info_success")
394
+ or "Server info retrieved successfully."
395
+ )
396
+ return {
397
+ "status": "success",
398
+ "message": success_message,
399
+ "raw_result": response.raw_result,
400
+ }
177
401
  except HTTPException as http_exc:
178
402
  raise http_exc
179
403
  except Exception as e:
180
404
  logger.error(f"Unexpected error during MCP info request: {str(e)}")
181
405
  raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
182
406
 
407
+
183
408
  # Potentially add endpoints for direct tool calls or resource access if needed in the future
184
409
  # @router.post("/api/mcp/call_tool")
185
410
  # async def call_mcp_tool(...): ...
186
411
 
187
412
  # @router.get("/api/mcp/read_resource")
188
- # async def read_mcp_resource(...): ...
413
+ # async def read_mcp_resource(...): ...
auto_coder_web/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.60"
1
+ __version__ = "0.1.62"