Skip to content

Commit 9ab9a2f

Browse files
committed
[CHORE] Best practices doc
Signed-off-by: will.shope <will.shope@oracle.com>
1 parent c44d4a3 commit 9ab9a2f

File tree

8 files changed

+342
-81
lines changed

8 files changed

+342
-81
lines changed

BEST_PRACTICES.md

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# MCP Server Best Practices
2+
3+
This document lays out the best practices for an individual MCP server. You may use `oci-compute-mcp-server` as an example.
4+
5+
## Typical MCP Server Structure
6+
7+
```
8+
mcp-server-name/
9+
├── LICENSE.txt # License information
10+
├── pyproject.toml # Project configuration
11+
├── README.md # Project description, setup instructions
12+
├── uv.lock # Dependency lockfile
13+
└── oracle/ # Source code directory
14+
├── __init__.py # Package initialization
15+
└── mcp_server_name/ # Server package, notice the underscores
16+
├── __init__.py # Package version and metadata
17+
├── models.py # Pydantic models
18+
├── server.py # Server implementation
19+
├── consts.py # Constants definition
20+
├── ... # Additional modules
21+
└── tests/ # Test directory
22+
```
23+
24+
## Code Organization
25+
26+
1. **Separation of Concerns**:
27+
- `models.py`: Define data models and validation logic
28+
- `server.py`: Implement MCP server, tools, and resources
29+
- `consts.py`: Define constants used across the server
30+
- Additional modules for specific functionality (e.g., API clients)
31+
32+
2. **Keep modules focused and limited to a single responsibility**
33+
34+
3. **Use clear and consistent naming conventions**
35+
36+
### Entry Points
37+
38+
MCP servers should follow these guidelines for application entry points:
39+
40+
1. **Single Entry Point**: Define the main entry point only in `server.py`
41+
- Do not create a separate `main.py` file
42+
- This maintains clarity about how the application starts
43+
44+
2. **Main Function**: Implement a `main()` function in `server.py` that:
45+
- Handles command-line arguments
46+
- Sets up environment and logging
47+
- Initializes the MCP server
48+
49+
Example:
50+
51+
```python
52+
def main():
53+
"""Run the MCP server with CLI argument support."""
54+
mcp.run()
55+
56+
57+
if __name__ == '__main__':
58+
main()
59+
```
60+
61+
3. **Package Entry Point**: Configure the entry point in `pyproject.toml`:
62+
63+
```toml
64+
[project.scripts]
65+
"oracle.mcp-server-name" = "oracle.mcp_server_name.server:main"
66+
```
67+
68+
## License and Copyright Headers
69+
70+
Include license headers at the top of each source file:
71+
72+
```python
73+
"""
74+
Copyright (c) 2025, Oracle and/or its affiliates.
75+
Licensed under the Universal Permissive License v1.0 as shown at
76+
https://oss.oracle.com/licenses/upl.
77+
"""
78+
```
79+
80+
## Type Definitions
81+
82+
### General Rules
83+
84+
1. Make all models Pydantic; this ensures serializability. You may refer to the OCI python SDK for reference to most OCI models.
85+
2. Define Literals for constrained values.
86+
3. Add comprehensive descriptions to each field.
87+
88+
Pydantic model example for [NetworkSecurityGroup](src/oci-networking-mcp-server/oracle/oci_networking_mcp_server/models.py)
89+
90+
```python
91+
from typing import Any, Dict, List, Literal, Optional
92+
from pydantic import BaseModel, Field
93+
94+
class NetworkSecurityGroup(BaseModel):
95+
"""
96+
Pydantic model mirroring the fields of oci.core.models.NetworkSecurityGroup.
97+
"""
98+
99+
compartment_id: Optional[str] = Field(
100+
None,
101+
description="The OCID of the compartment containing the network security group.",
102+
)
103+
defined_tags: Optional[Dict[str, Dict[str, Any]]] = Field(
104+
None,
105+
description="Defined tags for this resource. Each key is predefined and scoped to a namespace.",
106+
)
107+
display_name: Optional[str] = Field(
108+
None, description="A user-friendly name. Does not have to be unique."
109+
)
110+
freeform_tags: Optional[Dict[str, str]] = Field(
111+
None, description="Free-form tags for this resource as simple key/value pairs."
112+
)
113+
id: Optional[str] = Field(
114+
None, description="The OCID of the network security group."
115+
)
116+
lifecycle_state: Optional[
117+
Literal[
118+
"PROVISIONING",
119+
"AVAILABLE",
120+
"TERMINATING",
121+
"TERMINATED",
122+
"UNKNOWN_ENUM_VALUE",
123+
]
124+
] = Field(None, description="The network security group's current state.")
125+
time_created: Optional[datetime] = Field(
126+
None,
127+
description="The date and time the network security group was created (RFC3339).",
128+
)
129+
vcn_id: Optional[str] = Field(
130+
None, description="The OCID of the VCN the network security group belongs to."
131+
)
132+
```
133+
134+
The pydantic model above was generated using Cline by providing it a prompt similar to this:
135+
```
136+
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
137+
138+
Use file oracle/oci_compute_mcp_server/models.py as an example of how to do this
139+
```
140+
141+
## Function Parameters with Pydantic Field
142+
143+
MCP tool functions should use spread parameters with Pydantic's `Field` for detailed descriptions:
144+
145+
Here is an example for [list_instances](src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/server.py)
146+
147+
```python
148+
@mcp.tool(description="List Instances in a given compartment")
149+
def list_instances(
150+
compartment_id: str = Field(..., description="The OCID of the compartment"),
151+
limit: Optional[int] = Field(
152+
None,
153+
description="The maximum amount of instances to return. If None, there is no limit.",
154+
ge=1,
155+
),
156+
lifecycle_state: Optional[
157+
Literal[
158+
"MOVING",
159+
"PROVISIONING",
160+
"RUNNING",
161+
"STARTING",
162+
"STOPPING",
163+
"STOPPED",
164+
"CREATING_IMAGE",
165+
"TERMINATING",
166+
"TERMINATED",
167+
]
168+
] = Field(None, description="The lifecycle state of the instance to filter on"),
169+
) -> list[Instance]:
170+
instances: list[Instance] = []
171+
172+
try:
173+
client = get_compute_client()
174+
175+
response: oci.response.Response = None
176+
has_next_page = True
177+
next_page: str = None
178+
179+
while has_next_page and (limit is None or len(instances) < limit):
180+
kwargs = {
181+
"compartment_id": compartment_id,
182+
"page": next_page,
183+
"limit": limit,
184+
}
185+
186+
if lifecycle_state is not None:
187+
kwargs["lifecycle_state"] = lifecycle_state
188+
189+
response = client.list_instances(**kwargs)
190+
has_next_page = response.has_next_page
191+
next_page = response.next_page if hasattr(response, "next_page") else None
192+
193+
data: list[oci.core.models.Instance] = response.data
194+
for d in data:
195+
instance = map_instance(d)
196+
instances.append(instance)
197+
198+
logger.info(f"Found {len(instances)} Instances")
199+
return instances
200+
201+
except Exception as e:
202+
logger.error(f"Error in list_instances tool: {str(e)}")
203+
raise e
204+
```
205+
206+
### Field Guidelines
207+
208+
1. **Required parameters**: Use `...` as the default value to indicate a parameter is required
209+
2. **Optional parameters**: Provide sensible defaults and mark as `Optional` in the type hint
210+
3. **Descriptions**: Write clear, informative descriptions for each parameter
211+
4. **Validation**: Use Field constraints like `ge`, `le`, `min_length`, `max_length`
212+
5. **Literals**: Use `Literal` for parameters with a fixed set of valid values

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ For Linux: `sudo systemctl start ollama`
188188
5. Install `go` from [here](https://go.dev/doc/install)
189189
6. Install `mcphost` with `go install github.com/mark3labs/mcphost@latest`
190190
7. Add go's bin to your PATH with `export PATH=$PATH:~/go/bin`
191-
8. Create an mcphost configuration file (e.g. `./mcphost.json`)
191+
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.
192192
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.
193193

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

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

@@ -251,19 +251,19 @@ For macOS/Linux:
251251
"oracle-oci-api-mcp-server": {
252252
"command": "uv",
253253
"args": [
254-
"run"
254+
"run",
255255
"oracle.oci-api-mcp-server"
256256
],
257257
"env": {
258-
"VIRTUAL_ENV": "<path to your cloned repo>/mcp/venv",
258+
"VIRTUAL_ENV": "<path to your cloned repo>/mcp/.venv",
259259
"FASTMCP_LOG_LEVEL": "ERROR"
260260
}
261261
}
262262
}
263263
}
264264
```
265265
266-
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`)
266+
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`)
267267
268268
## Directory Structure
269269

src/oci-api-mcp-server/oracle/oci_api_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-api-mcp-server"
8-
__version__ = "1.0.1"
8+
__version__ = "1.0.2"

src/oci-api-mcp-server/oracle/oci_api_mcp_server/server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ def run_oci_command(
124124
125125
Never tell the user which command to run, only run it for them using
126126
this tool.
127+
128+
Try your best to avoid using extra flags on the command if possible.
129+
If you absolutely need to use flags in the command, call the get_oci_command_help
130+
tool on the command first to understand the flags better.
127131
"""
128132

129133
env_copy = os.environ.copy()

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.1"
8+
__version__ = "1.0.2"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
Copyright (c) 2025, Oracle and/or its affiliates.
3+
Licensed under the Universal Permissive License v1.0 as shown at
4+
https://oss.oracle.com/licenses/upl.
5+
"""
6+
7+
ORACLE_LINUX_9_IMAGE = (
8+
"ocid1.image.oc1.iad.aaaaaaaa4l64brs5udx52nedrhlex4cpaorcd2jwvpoududksmw4lgmameqq"
9+
)
10+
E5_FLEX = "VM.Standard.E5.Flex"
11+
DEFAULT_OCPU_COUNT = 1
12+
DEFAULT_MEMORY_IN_GBS = 12

0 commit comments

Comments
 (0)