azurefunctions-agents-runtime 0.0.0.dev1__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.
- azure_functions_agents/__init__.py +20 -0
- azure_functions_agents/app.py +720 -0
- azure_functions_agents/arm.py +95 -0
- azure_functions_agents/client_manager.py +84 -0
- azure_functions_agents/config.py +191 -0
- azure_functions_agents/connector_tool_cache.py +124 -0
- azure_functions_agents/connector_tools.py +267 -0
- azure_functions_agents/connectors.py +460 -0
- azure_functions_agents/mcp.py +87 -0
- azure_functions_agents/public/index.html +1504 -0
- azure_functions_agents/runner.py +406 -0
- azure_functions_agents/sandbox.py +288 -0
- azure_functions_agents/skills.py +24 -0
- azure_functions_agents/tools.py +316 -0
- azurefunctions_agents_runtime-0.0.0.dev1.dist-info/METADATA +386 -0
- azurefunctions_agents_runtime-0.0.0.dev1.dist-info/RECORD +20 -0
- azurefunctions_agents_runtime-0.0.0.dev1.dist-info/WHEEL +5 -0
- azurefunctions_agents_runtime-0.0.0.dev1.dist-info/licenses/LICENSE.md +21 -0
- azurefunctions_agents_runtime-0.0.0.dev1.dist-info/top_level.txt +2 -0
- copilot_functions/__init__.py +3 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from .arm import ArmClient, DataPlaneClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ParsedParameter:
|
|
11
|
+
name: str
|
|
12
|
+
location: str # "path", "query", "header", "body"
|
|
13
|
+
type: str
|
|
14
|
+
required: bool
|
|
15
|
+
description: str
|
|
16
|
+
format: str | None = None
|
|
17
|
+
enum: list[str] | None = None
|
|
18
|
+
default: object = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ParsedOperation:
|
|
23
|
+
operation_id: str
|
|
24
|
+
method: str
|
|
25
|
+
path: str
|
|
26
|
+
summary: str
|
|
27
|
+
description: str
|
|
28
|
+
parameters: list[ParsedParameter] = field(default_factory=list)
|
|
29
|
+
body_properties: list[ParsedParameter] = field(default_factory=list)
|
|
30
|
+
body_required_fields: list[str] = field(default_factory=list)
|
|
31
|
+
internal_params: list[ParsedParameter] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ConnectionInfo:
|
|
36
|
+
resource_id: str
|
|
37
|
+
name: str
|
|
38
|
+
api_name: str
|
|
39
|
+
display_name: str
|
|
40
|
+
status: str
|
|
41
|
+
location: str
|
|
42
|
+
operations: list[ParsedOperation] = field(default_factory=list)
|
|
43
|
+
connection_runtime_url: str | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_v2_connection(connection_id: str) -> bool:
|
|
47
|
+
"""Return True if the connection ID is a V2 (gateway) connection."""
|
|
48
|
+
lower = connection_id.lower()
|
|
49
|
+
return "/aigateways/" in lower or "/connectorgateways/" in lower
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve_ref(ref: str, root: dict) -> dict:
|
|
53
|
+
"""Resolve a $ref pointer like '#/definitions/Foo' against the swagger root."""
|
|
54
|
+
parts = ref.lstrip("#/").split("/")
|
|
55
|
+
result = root
|
|
56
|
+
for part in parts:
|
|
57
|
+
result = result.get(part, {})
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _resolve_schema(schema: dict, swagger: dict, depth: int = 0) -> dict:
|
|
62
|
+
"""Resolve a schema, following $ref if present."""
|
|
63
|
+
if "$ref" in schema:
|
|
64
|
+
return _resolve_ref(schema["$ref"], swagger)
|
|
65
|
+
return schema
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _extract_body_properties(
|
|
69
|
+
body_schema: dict, swagger: dict, max_depth: int = 2, depth: int = 0
|
|
70
|
+
) -> tuple[list[ParsedParameter], list[str]]:
|
|
71
|
+
"""Flatten body schema properties into a list of ParsedParameters."""
|
|
72
|
+
resolved = _resolve_schema(body_schema, swagger)
|
|
73
|
+
properties = resolved.get("properties", {})
|
|
74
|
+
required_fields = resolved.get("required", [])
|
|
75
|
+
params = []
|
|
76
|
+
|
|
77
|
+
internal_params = []
|
|
78
|
+
for prop_name, prop_schema in properties.items():
|
|
79
|
+
prop_resolved = _resolve_schema(prop_schema, swagger)
|
|
80
|
+
visibility = prop_resolved.get("x-ms-visibility", "")
|
|
81
|
+
if visibility == "internal":
|
|
82
|
+
if prop_resolved.get("default") is not None:
|
|
83
|
+
internal_params.append(ParsedParameter(
|
|
84
|
+
name=prop_name,
|
|
85
|
+
location="body",
|
|
86
|
+
type=prop_resolved.get("type", "string"),
|
|
87
|
+
required=False,
|
|
88
|
+
description="",
|
|
89
|
+
default=prop_resolved.get("default"),
|
|
90
|
+
))
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
prop_type = prop_resolved.get("type", "string")
|
|
94
|
+
|
|
95
|
+
# Flatten nested objects: extract their properties with dot-separated names
|
|
96
|
+
if prop_type == "object" and depth < max_depth:
|
|
97
|
+
nested_props = prop_resolved.get("properties", {})
|
|
98
|
+
nested_required = prop_resolved.get("required", [])
|
|
99
|
+
if nested_props:
|
|
100
|
+
for nested_name, nested_schema in nested_props.items():
|
|
101
|
+
nested_resolved = _resolve_schema(nested_schema, swagger)
|
|
102
|
+
nested_vis = nested_resolved.get("x-ms-visibility", "")
|
|
103
|
+
if nested_vis == "internal":
|
|
104
|
+
continue
|
|
105
|
+
nested_type = nested_resolved.get("type", "string")
|
|
106
|
+
if nested_type in ("object", "array") and depth + 1 >= max_depth:
|
|
107
|
+
nested_type = "string"
|
|
108
|
+
flat_name = f"{prop_name}.{nested_name}"
|
|
109
|
+
params.append(ParsedParameter(
|
|
110
|
+
name=flat_name,
|
|
111
|
+
location="body",
|
|
112
|
+
type=nested_type,
|
|
113
|
+
required=nested_name in nested_required,
|
|
114
|
+
description=nested_resolved.get("description", nested_resolved.get("x-ms-summary", nested_resolved.get("title", ""))),
|
|
115
|
+
format=nested_resolved.get("format"),
|
|
116
|
+
enum=nested_resolved.get("enum"),
|
|
117
|
+
default=nested_resolved.get("default"),
|
|
118
|
+
))
|
|
119
|
+
if nested_name in nested_required:
|
|
120
|
+
required_fields.append(flat_name)
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
if prop_type in ("object", "array") and depth >= max_depth:
|
|
124
|
+
prop_type = "string" # serialize as JSON string
|
|
125
|
+
|
|
126
|
+
params.append(ParsedParameter(
|
|
127
|
+
name=prop_name,
|
|
128
|
+
location="body",
|
|
129
|
+
type=prop_type,
|
|
130
|
+
required=prop_name in required_fields,
|
|
131
|
+
description=prop_resolved.get("description", prop_resolved.get("x-ms-summary", prop_resolved.get("title", ""))),
|
|
132
|
+
format=prop_resolved.get("format"),
|
|
133
|
+
enum=prop_resolved.get("enum"),
|
|
134
|
+
default=prop_resolved.get("default"),
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
return params, required_fields, internal_params
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def _resolve_dynamic_schema(
|
|
141
|
+
arm: ArmClient, resource_id: str, swagger: dict, dynamic_schema: dict, op: dict,
|
|
142
|
+
*, data_plane_client: DataPlaneClient | None = None, connection_runtime_url: str | None = None,
|
|
143
|
+
) -> dict | None:
|
|
144
|
+
"""Resolve an x-ms-dynamic-schema by calling the referenced operation."""
|
|
145
|
+
op_id = dynamic_schema.get("operationId")
|
|
146
|
+
if not op_id:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
# Find the path for the referenced operation
|
|
150
|
+
schema_path = None
|
|
151
|
+
schema_method = None
|
|
152
|
+
for p, methods in swagger.get("paths", {}).items():
|
|
153
|
+
for m, o in methods.items():
|
|
154
|
+
if isinstance(o, dict) and o.get("operationId") == op_id:
|
|
155
|
+
schema_path = p
|
|
156
|
+
schema_method = m
|
|
157
|
+
break
|
|
158
|
+
if schema_path:
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
if not schema_path:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
# Strip /{connectionId} from path
|
|
165
|
+
invoke_path = re.sub(r"^/\{connectionId\}", "", schema_path, flags=re.IGNORECASE)
|
|
166
|
+
|
|
167
|
+
# Build query/path params from the dynamic schema's parameters
|
|
168
|
+
params = dynamic_schema.get("parameters", {})
|
|
169
|
+
for param_name, param_val in params.items():
|
|
170
|
+
if isinstance(param_val, dict) and "parameter" in param_val:
|
|
171
|
+
ref_param = param_val["parameter"]
|
|
172
|
+
defaults = {"poster": "User", "location": "Channel", "recipientType": "Channel"}
|
|
173
|
+
param_val = defaults.get(ref_param, "")
|
|
174
|
+
invoke_path = invoke_path.replace(f"{{{param_name}}}", str(param_val))
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
if data_plane_client and connection_runtime_url:
|
|
178
|
+
# V2: direct HTTP to data plane
|
|
179
|
+
url = f"{connection_runtime_url.rstrip('/')}{invoke_path}"
|
|
180
|
+
result = await data_plane_client.request(schema_method.upper(), url)
|
|
181
|
+
value_path = dynamic_schema.get("value-path", "schema")
|
|
182
|
+
return result.get(value_path, result)
|
|
183
|
+
else:
|
|
184
|
+
# V1: dynamicInvoke via ARM
|
|
185
|
+
result = await arm.post(
|
|
186
|
+
f"{resource_id}/dynamicInvoke",
|
|
187
|
+
body={"request": {"method": schema_method.upper(), "path": invoke_path}}
|
|
188
|
+
)
|
|
189
|
+
response = result.get("response", {})
|
|
190
|
+
body = response.get("body", {})
|
|
191
|
+
value_path = dynamic_schema.get("value-path", "schema")
|
|
192
|
+
return body.get(value_path, body)
|
|
193
|
+
except Exception:
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
async def _parse_operations(
|
|
198
|
+
swagger: dict, arm: ArmClient, resource_id: str,
|
|
199
|
+
*, data_plane_client: DataPlaneClient | None = None, connection_runtime_url: str | None = None,
|
|
200
|
+
) -> list[ParsedOperation]:
|
|
201
|
+
"""Parse Swagger paths into a list of ParsedOperations."""
|
|
202
|
+
paths = swagger.get("paths", {})
|
|
203
|
+
operations: list[ParsedOperation] = []
|
|
204
|
+
seen_families: dict[str, tuple[ParsedOperation, int]] = {}
|
|
205
|
+
|
|
206
|
+
for path, methods in paths.items():
|
|
207
|
+
if "$subscriptions" in path:
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
for method, op in methods.items():
|
|
211
|
+
if method in ("parameters", "x-ms-notification-content"):
|
|
212
|
+
continue
|
|
213
|
+
if not isinstance(op, dict):
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
if op.get("x-ms-trigger"):
|
|
217
|
+
continue
|
|
218
|
+
if op.get("deprecated"):
|
|
219
|
+
continue
|
|
220
|
+
if method.lower() == "delete":
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
visibility = op.get("x-ms-visibility", "")
|
|
224
|
+
if visibility == "internal":
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
operation_id = op.get("operationId", f"{method}_{path}")
|
|
228
|
+
|
|
229
|
+
if operation_id.startswith("mcp_") or operation_id == "HttpRequest":
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
summary = op.get("summary", "")
|
|
233
|
+
description = op.get("description", "")
|
|
234
|
+
|
|
235
|
+
params = []
|
|
236
|
+
internal_params = []
|
|
237
|
+
body_props = []
|
|
238
|
+
body_required: list[str] = []
|
|
239
|
+
|
|
240
|
+
for param in op.get("parameters", []):
|
|
241
|
+
if "$ref" in param:
|
|
242
|
+
param = _resolve_ref(param["$ref"], swagger)
|
|
243
|
+
|
|
244
|
+
param_in = param.get("in", "")
|
|
245
|
+
if param_in == "body":
|
|
246
|
+
schema = param.get("schema", {})
|
|
247
|
+
resolved_schema = _resolve_schema(schema, swagger)
|
|
248
|
+
dynamic = resolved_schema.get("x-ms-dynamic-schema")
|
|
249
|
+
if dynamic and not resolved_schema.get("properties"):
|
|
250
|
+
dyn_schema = await _resolve_dynamic_schema(
|
|
251
|
+
arm, resource_id, swagger, dynamic, op,
|
|
252
|
+
data_plane_client=data_plane_client,
|
|
253
|
+
connection_runtime_url=connection_runtime_url,
|
|
254
|
+
)
|
|
255
|
+
if dyn_schema:
|
|
256
|
+
body_props, body_required, body_internal = _extract_body_properties(
|
|
257
|
+
{"properties": dyn_schema.get("properties", {}), "required": dyn_schema.get("required", [])},
|
|
258
|
+
swagger,
|
|
259
|
+
)
|
|
260
|
+
internal_params.extend(body_internal)
|
|
261
|
+
else:
|
|
262
|
+
body_props, body_required, body_internal = _extract_body_properties(schema, swagger)
|
|
263
|
+
internal_params.extend(body_internal)
|
|
264
|
+
else:
|
|
265
|
+
body_props, body_required, body_internal = _extract_body_properties(schema, swagger)
|
|
266
|
+
internal_params.extend(body_internal)
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
if param.get("name") == "connectionId":
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
param_visibility = param.get("x-ms-visibility", "")
|
|
273
|
+
if param_visibility == "internal":
|
|
274
|
+
if param.get("default") is not None:
|
|
275
|
+
internal_params.append(ParsedParameter(
|
|
276
|
+
name=param.get("name", ""),
|
|
277
|
+
location=param_in,
|
|
278
|
+
type=param.get("type", "string"),
|
|
279
|
+
required=False,
|
|
280
|
+
description="",
|
|
281
|
+
default=param.get("default"),
|
|
282
|
+
))
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
params.append(ParsedParameter(
|
|
286
|
+
name=param.get("name", ""),
|
|
287
|
+
location=param_in,
|
|
288
|
+
type=param.get("type", "string"),
|
|
289
|
+
required=param.get("required", False),
|
|
290
|
+
description=param.get("description", param.get("x-ms-summary", "")),
|
|
291
|
+
format=param.get("format"),
|
|
292
|
+
enum=param.get("enum"),
|
|
293
|
+
default=param.get("default"),
|
|
294
|
+
))
|
|
295
|
+
|
|
296
|
+
parsed = ParsedOperation(
|
|
297
|
+
operation_id=operation_id,
|
|
298
|
+
method=method.upper(),
|
|
299
|
+
path=path,
|
|
300
|
+
summary=summary,
|
|
301
|
+
description=description,
|
|
302
|
+
parameters=params,
|
|
303
|
+
body_properties=body_props,
|
|
304
|
+
body_required_fields=body_required,
|
|
305
|
+
internal_params=internal_params,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
annotation = op.get("x-ms-api-annotation", {})
|
|
309
|
+
family = annotation.get("family")
|
|
310
|
+
new_rev = annotation.get("revision", 0)
|
|
311
|
+
if family:
|
|
312
|
+
existing = seen_families.get(family)
|
|
313
|
+
if existing is None:
|
|
314
|
+
seen_families[family] = (parsed, new_rev)
|
|
315
|
+
operations.append(parsed)
|
|
316
|
+
else:
|
|
317
|
+
existing_op, existing_rev = existing
|
|
318
|
+
if new_rev > existing_rev:
|
|
319
|
+
operations.remove(existing_op)
|
|
320
|
+
seen_families[family] = (parsed, new_rev)
|
|
321
|
+
operations.append(parsed)
|
|
322
|
+
else:
|
|
323
|
+
operations.append(parsed)
|
|
324
|
+
|
|
325
|
+
return operations
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _parse_resource_id(resource_id: str) -> dict:
|
|
329
|
+
"""Extract subscription, resource group, and name from a V1 connection resource ID."""
|
|
330
|
+
pattern = (
|
|
331
|
+
r"/subscriptions/(?P<subscription>[^/]+)"
|
|
332
|
+
r"/resourceGroups/(?P<resource_group>[^/]+)"
|
|
333
|
+
r"/providers/Microsoft\.Web/connections/(?P<name>[^/]+)"
|
|
334
|
+
)
|
|
335
|
+
match = re.search(pattern, resource_id, re.IGNORECASE)
|
|
336
|
+
if not match:
|
|
337
|
+
raise ValueError(f"Invalid V1 connection resource ID: {resource_id}")
|
|
338
|
+
return match.groupdict()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _parse_v2_resource_id(resource_id: str) -> dict:
|
|
342
|
+
"""Extract subscription, resource group, gateway type, gateway, and name from a V2 connection resource ID."""
|
|
343
|
+
pattern = (
|
|
344
|
+
r"/subscriptions/(?P<subscription>[^/]+)"
|
|
345
|
+
r"/resourceGroups/(?P<resource_group>[^/]+)"
|
|
346
|
+
r"/providers/Microsoft\.Web/(?P<gateway_type>aigateways|connectorGateways)/(?P<gateway>[^/]+)"
|
|
347
|
+
r"/connections/(?P<name>[^/]+)"
|
|
348
|
+
)
|
|
349
|
+
match = re.search(pattern, resource_id, re.IGNORECASE)
|
|
350
|
+
if not match:
|
|
351
|
+
raise ValueError(f"Invalid V2 connection resource ID: {resource_id}")
|
|
352
|
+
return match.groupdict()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
_V2_API_VERSIONS = {
|
|
356
|
+
"aigateways": "2026-03-01-preview",
|
|
357
|
+
"connectorgateways": "2026-05-01-preview",
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
async def load_connection(
|
|
362
|
+
arm: ArmClient, resource_id: str,
|
|
363
|
+
*, data_plane_client: DataPlaneClient | None = None,
|
|
364
|
+
) -> ConnectionInfo:
|
|
365
|
+
"""Fetch connection metadata and Swagger spec, return a ConnectionInfo with parsed operations.
|
|
366
|
+
|
|
367
|
+
Automatically detects V1 vs V2 connections based on the resource ID format.
|
|
368
|
+
"""
|
|
369
|
+
if is_v2_connection(resource_id):
|
|
370
|
+
return await _load_v2_connection(arm, resource_id, data_plane_client=data_plane_client)
|
|
371
|
+
return await _load_v1_connection(arm, resource_id)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
async def _load_v1_connection(arm: ArmClient, resource_id: str) -> ConnectionInfo:
|
|
375
|
+
"""Load a V1 connection (Microsoft.Web/connections)."""
|
|
376
|
+
conn_data = await arm.get(resource_id)
|
|
377
|
+
props = conn_data.get("properties", {})
|
|
378
|
+
api_name = props.get("api", {}).get("name", "")
|
|
379
|
+
display_name = props.get("displayName", "")
|
|
380
|
+
statuses = props.get("statuses") or [{}]
|
|
381
|
+
status = props.get("overallStatus", statuses[0].get("status", "Unknown"))
|
|
382
|
+
location = conn_data.get("location", "")
|
|
383
|
+
|
|
384
|
+
parts = _parse_resource_id(resource_id)
|
|
385
|
+
swagger_path = (
|
|
386
|
+
f"/subscriptions/{parts['subscription']}"
|
|
387
|
+
f"/providers/Microsoft.Web/locations/{location}"
|
|
388
|
+
f"/managedApis/{api_name}"
|
|
389
|
+
)
|
|
390
|
+
api_data = await arm.get(swagger_path, params={"export": "true"})
|
|
391
|
+
swagger = api_data.get("properties", {}).get("swagger", {})
|
|
392
|
+
if not swagger.get("paths"):
|
|
393
|
+
swagger = api_data
|
|
394
|
+
|
|
395
|
+
operations = await _parse_operations(swagger, arm, resource_id)
|
|
396
|
+
|
|
397
|
+
return ConnectionInfo(
|
|
398
|
+
resource_id=resource_id,
|
|
399
|
+
name=parts["name"],
|
|
400
|
+
api_name=api_name,
|
|
401
|
+
display_name=display_name,
|
|
402
|
+
status=status,
|
|
403
|
+
location=location,
|
|
404
|
+
operations=operations,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
async def _load_v2_connection(
|
|
409
|
+
arm: ArmClient, resource_id: str,
|
|
410
|
+
*, data_plane_client: DataPlaneClient | None = None,
|
|
411
|
+
) -> ConnectionInfo:
|
|
412
|
+
"""Load a V2 connection (Microsoft.Web/aigateways or connectorGateways)."""
|
|
413
|
+
parts = _parse_v2_resource_id(resource_id)
|
|
414
|
+
gateway_type = parts["gateway_type"]
|
|
415
|
+
api_version = _V2_API_VERSIONS.get(gateway_type.lower(), "2026-05-01-preview")
|
|
416
|
+
|
|
417
|
+
# Get connection metadata
|
|
418
|
+
conn_data = await arm.get(resource_id, api_version=api_version)
|
|
419
|
+
props = conn_data.get("properties", {})
|
|
420
|
+
api_name = props.get("connectorName", "")
|
|
421
|
+
display_name = props.get("displayName", "")
|
|
422
|
+
status = props.get("overallStatus", "Unknown")
|
|
423
|
+
connection_runtime_url = props.get("connectionRuntimeUrl", "")
|
|
424
|
+
|
|
425
|
+
# Get location from the parent gateway
|
|
426
|
+
gateway_path = (
|
|
427
|
+
f"/subscriptions/{parts['subscription']}"
|
|
428
|
+
f"/resourceGroups/{parts['resource_group']}"
|
|
429
|
+
f"/providers/Microsoft.Web/{gateway_type}/{parts['gateway']}"
|
|
430
|
+
)
|
|
431
|
+
gateway_data = await arm.get(gateway_path, api_version=api_version)
|
|
432
|
+
location = gateway_data.get("location", "")
|
|
433
|
+
|
|
434
|
+
# Swagger uses the same managed API endpoint as V1
|
|
435
|
+
swagger_path = (
|
|
436
|
+
f"/subscriptions/{parts['subscription']}"
|
|
437
|
+
f"/providers/Microsoft.Web/locations/{location}"
|
|
438
|
+
f"/managedApis/{api_name}"
|
|
439
|
+
)
|
|
440
|
+
api_data = await arm.get(swagger_path, params={"export": "true"})
|
|
441
|
+
swagger = api_data.get("properties", {}).get("swagger", {})
|
|
442
|
+
if not swagger.get("paths"):
|
|
443
|
+
swagger = api_data
|
|
444
|
+
|
|
445
|
+
operations = await _parse_operations(
|
|
446
|
+
swagger, arm, resource_id,
|
|
447
|
+
data_plane_client=data_plane_client,
|
|
448
|
+
connection_runtime_url=connection_runtime_url,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
return ConnectionInfo(
|
|
452
|
+
resource_id=resource_id,
|
|
453
|
+
name=parts["name"],
|
|
454
|
+
api_name=api_name,
|
|
455
|
+
display_name=display_name,
|
|
456
|
+
status=status,
|
|
457
|
+
location=location,
|
|
458
|
+
operations=operations,
|
|
459
|
+
connection_runtime_url=connection_runtime_url,
|
|
460
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from copilot.session import MCPLocalServerConfig, MCPRemoteServerConfig, MCPServerConfig
|
|
7
|
+
|
|
8
|
+
from .config import get_app_root
|
|
9
|
+
|
|
10
|
+
_MCP_SERVERS_CACHE: Optional[Dict[str, MCPServerConfig]] = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parse_mcp_server_config(server: Dict[str, Any]) -> Optional[MCPServerConfig]:
|
|
14
|
+
server_type = str(server.get("type", "")).lower()
|
|
15
|
+
|
|
16
|
+
if "command" in server or server_type == "local":
|
|
17
|
+
local_config: MCPLocalServerConfig = {
|
|
18
|
+
"type": "local",
|
|
19
|
+
"command": str(server.get("command", "")),
|
|
20
|
+
"args": server.get("args", []),
|
|
21
|
+
"env": server.get("env", {}),
|
|
22
|
+
"tools": server.get("tools", ["*"]),
|
|
23
|
+
}
|
|
24
|
+
if not local_config["command"]:
|
|
25
|
+
return None
|
|
26
|
+
return local_config
|
|
27
|
+
|
|
28
|
+
if "url" in server or server_type in {"http", "sse"}:
|
|
29
|
+
remote_type = server_type if server_type in {"http", "sse"} else "http"
|
|
30
|
+
remote_config: MCPRemoteServerConfig = {
|
|
31
|
+
"type": remote_type, # type: ignore
|
|
32
|
+
"url": str(server.get("url", "")),
|
|
33
|
+
"headers": server.get("headers"),
|
|
34
|
+
"tools": server.get("tools", ["*"]),
|
|
35
|
+
}
|
|
36
|
+
if not remote_config["url"]:
|
|
37
|
+
return None
|
|
38
|
+
return remote_config
|
|
39
|
+
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_mcp_servers_from_file() -> Dict[str, MCPServerConfig]:
|
|
44
|
+
app_root = str(get_app_root())
|
|
45
|
+
candidates = [
|
|
46
|
+
os.path.join(app_root, ".vscode", "mcp.json"),
|
|
47
|
+
os.path.join(app_root, "mcp.json"),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
for path in candidates:
|
|
51
|
+
if not os.path.exists(path):
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
56
|
+
data = json.load(f)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
logging.warning(f"Failed to read MCP config from {path}: {e}")
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
servers = data.get("servers", {})
|
|
62
|
+
if not isinstance(servers, dict):
|
|
63
|
+
logging.warning(f"Invalid MCP config in {path}: 'servers' must be an object")
|
|
64
|
+
return {}
|
|
65
|
+
|
|
66
|
+
parsed_servers: Dict[str, MCPServerConfig] = {}
|
|
67
|
+
for name, config in servers.items():
|
|
68
|
+
if not isinstance(name, str) or not isinstance(config, dict):
|
|
69
|
+
continue
|
|
70
|
+
parsed = _parse_mcp_server_config(config)
|
|
71
|
+
if parsed is not None:
|
|
72
|
+
parsed_servers[name] = parsed
|
|
73
|
+
|
|
74
|
+
if parsed_servers:
|
|
75
|
+
logging.info(f"Loaded {len(parsed_servers)} MCP server(s) from {path}")
|
|
76
|
+
else:
|
|
77
|
+
logging.info(f"No valid MCP servers found in {path}")
|
|
78
|
+
return parsed_servers
|
|
79
|
+
|
|
80
|
+
return {}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_cached_mcp_servers() -> Dict[str, MCPServerConfig]:
|
|
84
|
+
global _MCP_SERVERS_CACHE
|
|
85
|
+
if _MCP_SERVERS_CACHE is None:
|
|
86
|
+
_MCP_SERVERS_CACHE = _load_mcp_servers_from_file()
|
|
87
|
+
return _MCP_SERVERS_CACHE
|