Skip to content

Commit 2737ca4

Browse files
committed
[CHORE] Add VNIC tools
Signed-off-by: will.shope <[email protected]>
1 parent d7e0fc6 commit 2737ca4

File tree

11 files changed

+494
-34
lines changed

11 files changed

+494
-34
lines changed

src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
"""
66

77
__project__ = "oracle.oci-compute-mcp-server"
8-
__version__ = "1.0.2"
8+
__version__ = "1.1.0"

src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/models.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,3 +781,104 @@ def map_response(resp: oci.response.Response) -> Response | None:
781781

782782

783783
# endregion
784+
785+
# region VnicAttachment
786+
787+
788+
class VnicAttachment(BaseModel):
789+
"""
790+
Pydantic model mirroring the fields of oci.core.models.VnicAttachment.
791+
"""
792+
793+
availability_domain: Optional[str] = Field(
794+
None,
795+
description="The availability domain of the instance. Example: `Uocm:PHX-AD-1`",
796+
)
797+
compartment_id: Optional[str] = Field(
798+
None,
799+
description=(
800+
"The OCID of the compartment the VNIC attachment is in, which is the "
801+
"same compartment the instance is in."
802+
),
803+
)
804+
display_name: Optional[str] = Field(
805+
None,
806+
description=(
807+
"A user-friendly name. Does not have to be unique, and it's changeable. "
808+
"Avoid entering confidential information."
809+
),
810+
)
811+
id: Optional[str] = Field(None, description="The OCID of the VNIC attachment.")
812+
instance_id: Optional[str] = Field(None, description="The OCID of the instance.")
813+
lifecycle_state: Optional[
814+
Literal["ATTACHING", "ATTACHED", "DETACHING", "DETACHED", "UNKNOWN_ENUM_VALUE"]
815+
] = Field(None, description="The current state of the VNIC attachment.")
816+
nic_index: Optional[int] = Field(
817+
None,
818+
description=(
819+
"Which physical network interface card (NIC) the VNIC uses. Certain bare "
820+
"metal instance shapes have two active physical NICs (0 and 1). If you add "
821+
"a secondary VNIC to one of these instances, you can specify which NIC "
822+
"the VNIC will use. For more information, see Virtual Network Interface "
823+
"Cards (VNICs)."
824+
),
825+
)
826+
subnet_id: Optional[str] = Field(
827+
None, description="The OCID of the subnet to create the VNIC in."
828+
)
829+
vlan_id: Optional[str] = Field(
830+
None,
831+
description=(
832+
"The OCID of the VLAN to create the VNIC in. Creating the VNIC in a VLAN "
833+
"(instead of a subnet) is possible only if you are an Oracle Cloud VMware "
834+
"Solution customer. See Vlan. An error is returned if the instance already "
835+
"has a VNIC attached to it from this VLAN."
836+
),
837+
)
838+
time_created: Optional[datetime] = Field(
839+
None,
840+
description=(
841+
"The date and time the VNIC attachment was created, in the format defined "
842+
"by RFC3339. Example: `2016-08-25T21:10:29.600Z`"
843+
),
844+
)
845+
vlan_tag: Optional[int] = Field(
846+
None,
847+
description=(
848+
"The Oracle-assigned VLAN tag of the attached VNIC. Available after the "
849+
"attachment process is complete. However, if the VNIC belongs to a VLAN "
850+
"as part of the Oracle Cloud VMware Solution, the `vlanTag` value is "
851+
"instead the value of the `vlanTag` attribute for the VLAN. See Vlan. "
852+
"Example: `0`"
853+
),
854+
)
855+
vnic_id: Optional[str] = Field(
856+
None,
857+
description=(
858+
"The OCID of the VNIC. Available after the attachment process is "
859+
"complete."
860+
),
861+
)
862+
863+
864+
def map_vnic_attachment(va: oci.core.models.VnicAttachment) -> VnicAttachment:
865+
"""
866+
Convert an oci.core.models.VnicAttachment to oracle.oci_compute_mcp_server.models.VnicAttachment.
867+
"""
868+
return VnicAttachment(
869+
availability_domain=getattr(va, "availability_domain", None),
870+
compartment_id=getattr(va, "compartment_id", None),
871+
display_name=getattr(va, "display_name", None),
872+
id=getattr(va, "id", None),
873+
instance_id=getattr(va, "instance_id", None),
874+
lifecycle_state=getattr(va, "lifecycle_state", None),
875+
nic_index=getattr(va, "nic_index", None),
876+
subnet_id=getattr(va, "subnet_id", None),
877+
vlan_id=getattr(va, "vlan_id", None),
878+
time_created=getattr(va, "time_created", None),
879+
vlan_tag=getattr(va, "vlan_tag", None),
880+
vnic_id=getattr(va, "vnic_id", None),
881+
)
882+
883+
884+
# endregion

src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/server.py

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
Image,
2121
Instance,
2222
Response,
23+
VnicAttachment,
2324
map_image,
2425
map_instance,
2526
map_response,
27+
map_vnic_attachment,
2628
)
2729
from pydantic import Field
2830

@@ -88,7 +90,7 @@ def list_instances(
8890
"limit": limit,
8991
}
9092

91-
if lifecycle_state is not None:
93+
if lifecycle_state:
9294
kwargs["lifecycle_state"] = lifecycle_state
9395

9496
response = client.list_instances(**kwargs)
@@ -272,6 +274,11 @@ def list_images(
272274
operating_system: Optional[str] = Field(
273275
None, description="The operating system to filter with"
274276
),
277+
limit: Optional[int] = Field(
278+
None,
279+
description="The maximum amount of resources to return. If None, there is no limit.",
280+
ge=1,
281+
),
275282
) -> list[Image]:
276283
images: list[Image] = []
277284

@@ -282,8 +289,14 @@ def list_images(
282289
has_next_page = True
283290
next_page: str = None
284291

285-
while has_next_page:
286-
response = client.list_images(compartment_id=compartment_id, page=next_page)
292+
while has_next_page and (limit is None or len(images) < limit):
293+
kwargs = {
294+
"compartment_id": compartment_id,
295+
"page": next_page,
296+
"limit": limit,
297+
}
298+
299+
response = client.list_images(**kwargs)
287300
has_next_page = response.has_next_page
288301
next_page = response.next_page if hasattr(response, "next_page") else None
289302

@@ -345,6 +358,78 @@ def instance_action(
345358
raise e
346359

347360

361+
@mcp.tool(
362+
description="List vnic attachments in a given compartment and/or on a given instance. "
363+
)
364+
def list_vnic_attachments(
365+
compartment_id: str = Field(
366+
...,
367+
description="The OCID of the compartment. "
368+
"If an instance_id is passed in, but no compartment_id is passed in,"
369+
"then the compartment OCID of the instance may be used as a default.",
370+
),
371+
instance_id: Optional[str] = Field(None, description="The OCID of the instance"),
372+
limit: Optional[int] = Field(
373+
None,
374+
description="The maximum amount of resources to return. If None, there is no limit.",
375+
ge=1,
376+
),
377+
) -> list[VnicAttachment]:
378+
vnic_attachments: list[VnicAttachment] = []
379+
380+
try:
381+
client = get_compute_client()
382+
383+
response: oci.response.Response = None
384+
has_next_page = True
385+
next_page: str = None
386+
387+
while has_next_page and (limit is None or len(vnic_attachments) < limit):
388+
kwargs = {
389+
"compartment_id": compartment_id,
390+
"page": next_page,
391+
"limit": limit,
392+
}
393+
394+
if instance_id:
395+
kwargs["instance_id"] = instance_id
396+
397+
response = client.list_vnic_attachments(**kwargs)
398+
has_next_page = response.has_next_page
399+
next_page = response.next_page if hasattr(response, "next_page") else None
400+
401+
data: list[oci.core.models.VnicAttachment] = response.data
402+
403+
for d in data:
404+
vnic_attachments.append(map_vnic_attachment(d))
405+
406+
logger.info(f"Found {len(vnic_attachments)} Vnic Attachments")
407+
return vnic_attachments
408+
409+
except Exception as e:
410+
logger.error(f"Error in list_vnic_attachments tool: {str(e)}")
411+
raise e
412+
413+
414+
@mcp.tool(description="Get Vnic Attachment with a given OCID")
415+
def get_vnic_attachment(
416+
vnic_attachment_id: str = Field(..., description="The OCID of the vnic attachment")
417+
) -> VnicAttachment:
418+
try:
419+
client = get_compute_client()
420+
421+
response: oci.response.Response = client.get_vnic_attachment(
422+
vnic_attachment_id=vnic_attachment_id
423+
)
424+
data: oci.core.models.VnicAttachment = response.data
425+
logger.info("Found Vnic Attachment")
426+
return map_vnic_attachment(data)
427+
428+
except Exception as e:
429+
logger.error(f"Error in get_vnic_attachment tool: {str(e)}")
430+
raise e
431+
432+
348433
def main() -> None:
349434
mcp.run()
350435

src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/tests/test_compute_tools.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,55 @@ async def test_instance_action(self, mock_get_client):
227227

228228
assert result["id"] == "instance1"
229229
assert result["lifecycle_state"] == "STOPPING"
230+
231+
@pytest.mark.asyncio
232+
@patch("oracle.oci_compute_mcp_server.server.get_compute_client")
233+
async def test_list_vnic_attachments(self, mock_get_client):
234+
mock_client = MagicMock()
235+
mock_get_client.return_value = mock_client
236+
237+
mock_list_response = create_autospec(oci.response.Response)
238+
mock_list_response.data = [
239+
oci.core.models.VnicAttachment(
240+
id="vnicattachment1",
241+
display_name="VNIC attachment 1",
242+
lifecycle_state="ATTACHED",
243+
)
244+
]
245+
mock_list_response.has_next_page = False
246+
mock_list_response.next_page = None
247+
mock_client.list_vnic_attachments.return_value = mock_list_response
248+
249+
async with Client(mcp) as client:
250+
call_tool_result = await client.call_tool(
251+
"list_vnic_attachments",
252+
{
253+
"compartment_id": "test_compartment",
254+
},
255+
)
256+
result = call_tool_result.structured_content["result"]
257+
258+
assert len(result) == 1
259+
assert result[0]["id"] == "vnicattachment1"
260+
261+
@pytest.mark.asyncio
262+
@patch("oracle.oci_compute_mcp_server.server.get_compute_client")
263+
async def test_get_vnic_attachment(self, mock_get_client):
264+
mock_client = MagicMock()
265+
mock_get_client.return_value = mock_client
266+
267+
mock_get_response = create_autospec(oci.response.Response)
268+
mock_get_response.data = oci.core.models.VnicAttachment(
269+
id="vnicattachment1",
270+
display_name="VNIC attachment 1",
271+
lifecycle_state="ATTACHED",
272+
)
273+
mock_client.get_vnic_attachment.return_value = mock_get_response
274+
275+
async with Client(mcp) as client:
276+
call_tool_result = await client.call_tool(
277+
"get_vnic_attachment", {"vnic_attachment_id": "vnicattachment1"}
278+
)
279+
result = call_tool_result.structured_content
280+
281+
assert result["id"] == "vnicattachment1"

src/oci-compute-mcp-server/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "oracle.oci-compute-mcp-server"
3-
version = "1.0.2"
3+
version = "1.1.0"
44
description = "OCI Compute Service MCP server"
55
readme = "README.md"
66
requires-python = ">=3.13"

0 commit comments

Comments
 (0)