Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions BEST_PRACTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# MCP Server Best Practices

This document lays out the best practices for an individual MCP server. You may use `oci-compute-mcp-server` as an example.

## Typical MCP Server Structure

```
mcp-server-name/
├── LICENSE.txt # License information
├── pyproject.toml # Project configuration
├── README.md # Project description, setup instructions
├── uv.lock # Dependency lockfile
└── oracle/ # Source code directory
├── __init__.py # Package initialization
└── mcp_server_name/ # Server package, notice the underscores
├── __init__.py # Package version and metadata
├── models.py # Pydantic models
├── server.py # Server implementation
├── consts.py # Constants definition
├── ... # Additional modules
└── tests/ # Test directory
```

## Code Organization

1. **Separation of Concerns**:
- `models.py`: Define data models and validation logic
- `server.py`: Implement MCP server, tools, and resources
- `consts.py`: Define constants used across the server
- Additional modules for specific functionality (e.g., API clients)

2. **Keep modules focused and limited to a single responsibility**

3. **Use clear and consistent naming conventions**

### Entry Points

MCP servers should follow these guidelines for application entry points:

1. **Single Entry Point**: Define the main entry point only in `server.py`
- Do not create a separate `main.py` file
- This maintains clarity about how the application starts

2. **Main Function**: Implement a `main()` function in `server.py` that:
- Handles command-line arguments
- Sets up environment and logging
- Initializes the MCP server

Example:

```python
def main():
"""Run the MCP server with CLI argument support."""
mcp.run()


if __name__ == '__main__':
main()
```

3. **Package Entry Point**: Configure the entry point in `pyproject.toml`:

```toml
[project.scripts]
"oracle.mcp-server-name" = "oracle.mcp_server_name.server:main"
```

## License and Copyright Headers

Include license headers at the top of each source file:

```python
"""
Copyright (c) 2025, Oracle and/or its affiliates.
Licensed under the Universal Permissive License v1.0 as shown at
https://oss.oracle.com/licenses/upl.
"""
```

## Type Definitions

### General Rules

1. Make all models Pydantic; this ensures serializability. You may refer to the OCI python SDK for reference to most OCI models.
2. Define Literals for constrained values.
3. Add comprehensive descriptions to each field.

Pydantic model example for [NetworkSecurityGroup](src/oci-networking-mcp-server/oracle/oci_networking_mcp_server/models.py)

```python
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, Field

class NetworkSecurityGroup(BaseModel):
"""
Pydantic model mirroring the fields of oci.core.models.NetworkSecurityGroup.
"""

compartment_id: Optional[str] = Field(
None,
description="The OCID of the compartment containing the network security group.",
)
defined_tags: Optional[Dict[str, Dict[str, Any]]] = Field(
None,
description="Defined tags for this resource. Each key is predefined and scoped to a namespace.",
)
display_name: Optional[str] = Field(
None, description="A user-friendly name. Does not have to be unique."
)
freeform_tags: Optional[Dict[str, str]] = Field(
None, description="Free-form tags for this resource as simple key/value pairs."
)
id: Optional[str] = Field(
None, description="The OCID of the network security group."
)
lifecycle_state: Optional[
Literal[
"PROVISIONING",
"AVAILABLE",
"TERMINATING",
"TERMINATED",
"UNKNOWN_ENUM_VALUE",
]
] = Field(None, description="The network security group's current state.")
time_created: Optional[datetime] = Field(
None,
description="The date and time the network security group was created (RFC3339).",
)
vcn_id: Optional[str] = Field(
None, description="The OCID of the VCN the network security group belongs to."
)
```

The pydantic model above was generated using Cline by providing it a prompt similar to this:
```
Can you create a pydantic model of oci.core.models.NetworkSecurityGroup and put it inside of the oracle/oci_networking_mcp_server/models.py file, and name it NetworkSecurityGroup? Can you also make a function that maps an oci.core.models.NetworkSecurityGroup instance to an oracle.oci_networking_mcp_server.model.NetworkSecurityGroup instance? Do the same for all of the nested types within the model as well

Use file oracle/oci_compute_mcp_server/models.py as an example of how to do this
```

## Function Parameters with Pydantic Field

MCP tool functions should use spread parameters with Pydantic's `Field` for detailed descriptions:

Here is an example for [list_instances](src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/server.py)

```python
@mcp.tool(description="List Instances in a given compartment")
def list_instances(
compartment_id: str = Field(..., description="The OCID of the compartment"),
limit: Optional[int] = Field(
None,
description="The maximum amount of instances to return. If None, there is no limit.",
ge=1,
),
lifecycle_state: Optional[
Literal[
"MOVING",
"PROVISIONING",
"RUNNING",
"STARTING",
"STOPPING",
"STOPPED",
"CREATING_IMAGE",
"TERMINATING",
"TERMINATED",
]
] = Field(None, description="The lifecycle state of the instance to filter on"),
) -> list[Instance]:
instances: list[Instance] = []

try:
client = get_compute_client()

response: oci.response.Response = None
has_next_page = True
next_page: str = None

while has_next_page and (limit is None or len(instances) < limit):
kwargs = {
"compartment_id": compartment_id,
"page": next_page,
"limit": limit,
}

if lifecycle_state is not None:
kwargs["lifecycle_state"] = lifecycle_state

response = client.list_instances(**kwargs)
has_next_page = response.has_next_page
next_page = response.next_page if hasattr(response, "next_page") else None

data: list[oci.core.models.Instance] = response.data
for d in data:
instance = map_instance(d)
instances.append(instance)

logger.info(f"Found {len(instances)} Instances")
return instances

except Exception as e:
logger.error(f"Error in list_instances tool: {str(e)}")
raise e
```

### Field Guidelines

1. **Required parameters**: Use `...` as the default value to indicate a parameter is required
2. **Optional parameters**: Provide sensible defaults and mark as `Optional` in the type hint
3. **Descriptions**: Write clear, informative descriptions for each parameter
4. **Validation**: Use Field constraints like `ge`, `le`, `min_length`, `max_length`
5. **Literals**: Use `Literal` for parameters with a fixed set of valid values
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ For Linux: `sudo systemctl start ollama`
5. Install `go` from [here](https://go.dev/doc/install)
6. Install `mcphost` with `go install github.com/mark3labs/mcphost@latest`
7. Add go's bin to your PATH with `export PATH=$PATH:~/go/bin`
8. Create an mcphost configuration file (e.g. `./mcphost.json`)
8. Create an mcphost configuration file (e.g. `~/.mcphost.json`). Check [here](https://github.com/mark3labs/mcphost?tab=readme-ov-file#mcp-servers) for more info.
9. Add your desired server to the `mcpServers` object. Below is an example for for the compute OCI MCP server. Make sure to save the file after editing.

For macOS/Linux:
Expand Down Expand Up @@ -227,8 +227,8 @@ This section will help you set up your environment to prepare it for local devel

1. Set up python virtual environment and install dev requirements
```sh
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
uv venv --python 3.13 --seed
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install -r requirements-dev.txt
```

Expand All @@ -251,19 +251,19 @@ For macOS/Linux:
"oracle-oci-api-mcp-server": {
"command": "uv",
"args": [
"run"
"run",
"oracle.oci-api-mcp-server"
],
"env": {
"VIRTUAL_ENV": "<path to your cloned repo>/mcp/venv",
"VIRTUAL_ENV": "<path to your cloned repo>/mcp/.venv",
"FASTMCP_LOG_LEVEL": "ERROR"
}
}
}
}
```

where `<path to your cloned repo>` is the absolute path to wherever you cloned this repo that will help point to the venv created above (e.g. `/Users/myuser/dev/mcp/venv`)
where `<path to your cloned repo>` is the absolute path to wherever you cloned this repo that will help point to the venv created above (e.g. `/Users/myuser/dev/mcp/.venv`)

## Directory Structure

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"""

__project__ = "oracle.oci-api-mcp-server"
__version__ = "1.0.1"
__version__ = "1.0.2"
4 changes: 4 additions & 0 deletions src/oci-api-mcp-server/oracle/oci_api_mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ def run_oci_command(

Never tell the user which command to run, only run it for them using
this tool.

Try your best to avoid using extra flags on the command if possible.
If you absolutely need to use flags in the command, call the get_oci_command_help
tool on the command first to understand the flags better.
"""

env_copy = os.environ.copy()
Expand Down
2 changes: 1 addition & 1 deletion src/oci-api-mcp-server/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "oracle.oci-api-mcp-server"
version = "1.0.1"
version = "1.0.2"
description = "OCI CLI MCP server"
readme = "README.md"
requires-python = ">=3.13"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"""

__project__ = "oracle.oci-compute-mcp-server"
__version__ = "1.0.1"
__version__ = "1.0.2"
12 changes: 12 additions & 0 deletions src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/consts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""
Copyright (c) 2025, Oracle and/or its affiliates.
Licensed under the Universal Permissive License v1.0 as shown at
https://oss.oracle.com/licenses/upl.
"""

ORACLE_LINUX_9_IMAGE = (
"ocid1.image.oc1.iad.aaaaaaaa4l64brs5udx52nedrhlex4cpaorcd2jwvpoududksmw4lgmameqq"
)
E5_FLEX = "VM.Standard.E5.Flex"
DEFAULT_OCPU_COUNT = 1
DEFAULT_MEMORY_IN_GBS = 12
Loading