Skip to content

Commit 119af70

Browse files
authored
♻️ Improvement: API to MCP conversion service supports configuring headers. (#3194)
* ♻️ Improvement: API to MCP conversion service supports configuring headers. [Specification Details] 1. Front-end and back-end modifications * ♻️ Improvement: API to MCP conversion service supports configuring headers. [Specification Details] 1. Modify the frontend, after adding, set the HTTP headers to empty. 2. Modify test cases.
1 parent 2273fd8 commit 119af70

8 files changed

Lines changed: 271 additions & 33 deletions

File tree

backend/apps/tool_config_app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,14 @@ async def import_openapi_service_api(
160160
server_url: Base URL of the REST API server
161161
openapi_json: Complete OpenAPI JSON specification
162162
service_description: Optional service description
163+
headers_template: Optional default headers template
163164
force_update: If True, replace all existing tools for this service
164165
"""
165166
service_name = openapi_service_request.get("service_name")
166167
server_url = openapi_service_request.get("server_url")
167168
openapi_json = openapi_service_request.get("openapi_json")
168169
service_description = openapi_service_request.get("service_description")
170+
headers_template = openapi_service_request.get("headers_template")
169171
force_update = openapi_service_request.get("force_update", False)
170172

171173
if not service_name:
@@ -192,6 +194,7 @@ async def import_openapi_service_api(
192194
tenant_id=tenant_id,
193195
user_id=user_id,
194196
service_description=service_description,
197+
headers_template=headers_template,
195198
force_update=force_update
196199
)
197200

backend/mcp_service.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,8 @@ def _sanitize_function_name(name: str) -> str:
188188
def register_openapi_service(
189189
service_name: str,
190190
openapi_json: Dict[str, Any],
191-
server_url: str
191+
server_url: str,
192+
headers_template: Dict[str, str],
192193
) -> bool:
193194
"""
194195
Register an OpenAPI service using FastMCP.from_openapi().
@@ -222,7 +223,7 @@ def register_openapi_service(
222223
openapi_spec["servers"] = [{"url": server_url}]
223224

224225
# Create HTTP client for the underlying REST API
225-
client = httpx.AsyncClient(base_url=server_url, timeout=30.0)
226+
client = httpx.AsyncClient(base_url=server_url, timeout=120.0, headers=headers_template)
226227

227228
# Create FastMCP instance from OpenAPI spec
228229
mcp_server = FastMCP.from_openapi(
@@ -320,13 +321,14 @@ def refresh_openapi_services_by_tenant(tenant_id: str) -> Dict[str, Any]:
320321
service_name = service.get("mcp_service_name")
321322
openapi_json = service.get("openapi_json")
322323
server_url = service.get("server_url")
324+
headers_template = service.get("headers_template")
323325

324326
if not openapi_json:
325327
logger.warning(f"Service '{service_name}' has no OpenAPI JSON, skipping")
326328
skipped_count += 1
327329
continue
328330

329-
if register_openapi_service(service_name, openapi_json, server_url):
331+
if register_openapi_service(service_name, openapi_json, server_url, headers_template):
330332
registered_count += 1
331333
else:
332334
skipped_count += 1
@@ -394,6 +396,7 @@ def refresh_single_openapi_service(service_name: str, tenant_id: str) -> Dict[st
394396
# Re-register with fresh data
395397
openapi_json = service_data.get("openapi_json")
396398
server_url = service_data.get("server_url")
399+
headers_template = service_data.get("headers_template")
397400

398401
if not openapi_json:
399402
logger.warning(f"Service '{service_name}' has no OpenAPI JSON")
@@ -403,7 +406,7 @@ def refresh_single_openapi_service(service_name: str, tenant_id: str) -> Dict[st
403406
"error": "No OpenAPI JSON found"
404407
}
405408

406-
success = register_openapi_service(service_name, openapi_json, server_url)
409+
success = register_openapi_service(service_name, openapi_json, server_url, headers_template)
407410
return {
408411
"status": "refreshed" if success else "error",
409412
"service_name": service_name,

backend/services/tool_configuration_service.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,7 @@ def import_openapi_service(
982982
tenant_id: str,
983983
user_id: str,
984984
service_description: str = None,
985+
headers_template: Dict[str, Any] = None,
985986
force_update: bool = False
986987
) -> Dict[str, Any]:
987988
"""
@@ -995,6 +996,7 @@ def import_openapi_service(
995996
tenant_id: Tenant ID for multi-tenancy
996997
user_id: User ID for audit
997998
service_description: Optional service description (if not provided, reads from openapi_json.info.description)
999+
headers_template: Optional default headers template
9981000
force_update: If True, replace all existing tools for this service
9991001
10001002
Returns:
@@ -1015,7 +1017,8 @@ def import_openapi_service(
10151017
server_url=server_url,
10161018
tenant_id=tenant_id,
10171019
user_id=user_id,
1018-
description=service_description
1020+
description=service_description,
1021+
headers_template=headers_template,
10191022
)
10201023

10211024
logger.info(f"Imported service '{service_name}' for tenant {tenant_id}")

frontend/app/[locale]/agents/components/agentConfig/McpConfigModal.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export default function McpConfigModal({
8080
const [openApiJson, setOpenApiJson] = useState("");
8181
const [openApiServiceName, setOpenApiServiceName] = useState("");
8282
const [openApiServerUrl, setOpenApiServerUrl] = useState("");
83+
const [openApiHeadersTemplate, setOpenApiHeadersTemplate] = useState("");
8384
const [importingOpenApi, setImportingOpenApi] = useState(false);
8485
const [openapiServices, setOpenapiServices] = useState<any[]>([]);
8586
const [loadingOpenapiServices, setLoadingOpenapiServices] = useState(false);
@@ -506,6 +507,7 @@ export default function McpConfigModal({
506507
service_name: openApiServiceName.trim(),
507508
server_url: openApiServerUrl.trim(),
508509
openapi_json: parsedJson,
510+
headers_template: openApiHeadersTemplate.trim() ? JSON.parse(openApiHeadersTemplate.trim()) : null,
509511
}),
510512
});
511513

@@ -514,6 +516,7 @@ export default function McpConfigModal({
514516
setOpenApiJson("");
515517
setOpenApiServiceName("");
516518
setOpenApiServerUrl("");
519+
setOpenApiHeadersTemplate("");
517520
await loadOpenapiServices();
518521
await refreshToolsAndAgents();
519522
} else {
@@ -1220,15 +1223,20 @@ export default function McpConfigModal({
12201223
style={{ flex: 3 }}
12211224
/>
12221225
</div>
1223-
<div>
1224-
<Input.TextArea
1225-
placeholder={t("mcpConfig.openApiToMcp.jsonPlaceholder")}
1226-
value={openApiJson}
1227-
onChange={(e) => setOpenApiJson(e.target.value)}
1228-
rows={6}
1229-
disabled={actionsLocked || importingOpenApi}
1230-
/>
1231-
</div>
1226+
<Input.TextArea
1227+
placeholder={t("mcpConfig.addServer.customHeadersPlaceholder")}
1228+
value={openApiHeadersTemplate}
1229+
onChange={(e) => setOpenApiHeadersTemplate(e.target.value)}
1230+
rows={2}
1231+
disabled={actionsLocked || importingOpenApi}
1232+
/>
1233+
<Input.TextArea
1234+
placeholder={t("mcpConfig.openApiToMcp.jsonPlaceholder")}
1235+
value={openApiJson}
1236+
onChange={(e) => setOpenApiJson(e.target.value)}
1237+
rows={6}
1238+
disabled={actionsLocked || importingOpenApi}
1239+
/>
12321240
<div
12331241
style={{
12341242
display: "flex",

frontend/app/[locale]/tenant-resources/components/resources/McpList.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export default function McpList({ tenantId }: { tenantId: string | null }) {
114114
const [openApiJson, setOpenApiJson] = useState("");
115115
const [openApiServiceName, setOpenApiServiceName] = useState("");
116116
const [openApiServerUrl, setOpenApiServerUrl] = useState("");
117+
const [openApiHeadersTemplate, setOpenApiHeadersTemplate] = useState("");
117118
const [importingOpenApi, setImportingOpenApi] = useState(false);
118119
const [openapiServices, setOpenapiServices] = useState<any[]>([]);
119120
const [loadingOpenapiServices, setLoadingOpenapiServices] = useState(false);
@@ -445,6 +446,7 @@ export default function McpList({ tenantId }: { tenantId: string | null }) {
445446
service_name: openApiServiceName.trim(),
446447
server_url: openApiServerUrl.trim(),
447448
openapi_json: parsedJson,
449+
headers_template: openApiHeadersTemplate.trim() ? JSON.parse(openApiHeadersTemplate.trim()) : null,
448450
}),
449451
});
450452

@@ -453,6 +455,7 @@ export default function McpList({ tenantId }: { tenantId: string | null }) {
453455
setOpenApiJson("");
454456
setOpenApiServiceName("");
455457
setOpenApiServerUrl("");
458+
setOpenApiHeadersTemplate("");
456459
await loadOpenapiServices();
457460
} else {
458461
const errorData = await response.json();
@@ -1035,13 +1038,22 @@ export default function McpList({ tenantId }: { tenantId: string | null }) {
10351038
style={{ flex: 3 }}
10361039
/>
10371040
</div>
1038-
<Input.TextArea
1039-
placeholder={t("mcpConfig.openApiToMcp.jsonPlaceholder")}
1040-
value={openApiJson}
1041-
onChange={(e) => setOpenApiJson(e.target.value)}
1042-
rows={6}
1043-
disabled={actionsLocked || importingOpenApi}
1044-
/>
1041+
<div className="space-y-2">
1042+
<Input.TextArea
1043+
placeholder={t("mcpConfig.addServer.customHeadersPlaceholder")}
1044+
value={openApiHeadersTemplate}
1045+
onChange={(e) => setOpenApiHeadersTemplate(e.target.value)}
1046+
rows={2}
1047+
disabled={actionsLocked || importingOpenApi}
1048+
/>
1049+
<Input.TextArea
1050+
placeholder={t("mcpConfig.openApiToMcp.jsonPlaceholder")}
1051+
value={openApiJson}
1052+
onChange={(e) => setOpenApiJson(e.target.value)}
1053+
rows={6}
1054+
disabled={actionsLocked || importingOpenApi}
1055+
/>
1056+
</div>
10451057
<div className="flex justify-end">
10461058
<Button
10471059
type="primary"

test/backend/app/test_tool_config_app.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,60 @@ def test_import_openapi_service_success(
533533
tenant_id="tenant456",
534534
user_id="user123",
535535
service_description="Test API",
536+
headers_template=None,
537+
force_update=False
538+
)
539+
mock_refresh_mcp.assert_called_once_with("tenant456")
540+
541+
@patch('apps.tool_config_app._refresh_openapi_services_in_mcp')
542+
@patch('apps.tool_config_app.get_current_user_id')
543+
@patch('apps.tool_config_app.import_openapi_service')
544+
def test_import_openapi_service_success_with_headers_template(
545+
self, mock_import_service, mock_get_user_id, mock_refresh_mcp
546+
):
547+
"""Test successful OpenAPI service import with headers template"""
548+
mock_get_user_id.return_value = ("user123", "tenant456")
549+
mock_import_service.return_value = {
550+
"tools_created": 1,
551+
"tools_updated": 0,
552+
"tools_deleted": 0
553+
}
554+
mock_refresh_mcp.return_value = {"status": "refreshed"}
555+
headers_template = {
556+
"Authorization": "Bearer {{token}}",
557+
"X-Tenant-ID": "{{tenant_id}}"
558+
}
559+
560+
response = client.post(
561+
"/tool/openapi_service",
562+
json={
563+
"service_name": "test_service",
564+
"server_url": "https://api.example.com",
565+
"openapi_json": {"openapi": "3.0.0", "info": {"title": "Test"}, "paths": {}},
566+
"service_description": "Test API",
567+
"headers_template": headers_template,
568+
"force_update": False
569+
}
570+
)
571+
572+
assert response.status_code == HTTPStatus.OK
573+
data = response.json()
574+
assert data["status"] == "success"
575+
assert data["message"] == "OpenAPI service import successful"
576+
assert data["data"]["tools_created"] == 1
577+
assert data["data"]["tools_updated"] == 0
578+
assert data["data"]["tools_deleted"] == 0
579+
assert data["data"]["mcp_refresh"]["status"] == "refreshed"
580+
581+
mock_get_user_id.assert_called_once_with(None)
582+
mock_import_service.assert_called_once_with(
583+
service_name="test_service",
584+
openapi_json={"openapi": "3.0.0", "info": {"title": "Test"}, "paths": {}},
585+
server_url="https://api.example.com",
586+
tenant_id="tenant456",
587+
user_id="user123",
588+
service_description="Test API",
589+
headers_template=headers_template,
536590
force_update=False
537591
)
538592
mock_refresh_mcp.assert_called_once_with("tenant456")

0 commit comments

Comments
 (0)