Skip to content

Commit 6b05fb2

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

File tree

6 files changed

+259
-27
lines changed

6 files changed

+259
-27
lines changed

BEST_PRACTICES.md

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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+
## Function Parameters with Pydantic Field
135+
136+
MCP tool functions should use spread parameters with Pydantic's `Field` for detailed descriptions:
137+
138+
Here is an example for [list_instances](src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/server.py)
139+
140+
```python
141+
@mcp.tool(description="List Instances in a given compartment")
142+
def list_instances(
143+
compartment_id: str = Field(
144+
...,
145+
description="The OCID of the compartment"
146+
),
147+
limit: Optional[int] = Field(
148+
None,
149+
description="The maximum amount of instances to return. If None, there is no limit.",
150+
ge=1
151+
),
152+
lifecycle_state: Optional[LifecycleState] = Field(
153+
None,
154+
description="The lifecycle state of the instance to filter on"
155+
)
156+
) -> list[Instance]:
157+
instances: list[Instance] = []
158+
159+
try:
160+
client = get_compute_client()
161+
162+
response: oci.response.Response = None
163+
has_next_page = True
164+
next_page: str = None
165+
166+
while has_next_page and (limit is None or len(instances) < limit):
167+
kwargs = {
168+
"compartment_id": compartment_id,
169+
"page": next_page,
170+
"limit": limit,
171+
}
172+
173+
if lifecycle_state is not None:
174+
kwargs["lifecycle_state"] = lifecycle_state
175+
176+
response = client.list_instances(**kwargs)
177+
has_next_page = response.has_next_page
178+
next_page = response.next_page if hasattr(response, "next_page") else None
179+
180+
data: list[oci.core.models.Instance] = response.data
181+
for d in data:
182+
instance = map_instance(d)
183+
instances.append(instance)
184+
185+
logger.info(f"Found {len(instances)} Instances")
186+
return instances
187+
188+
except Exception as e:
189+
logger.error(f"Error in list_instances tool: {str(e)}")
190+
raise e
191+
```
192+
193+
### Field Guidelines
194+
195+
1. **Required parameters**: Use `...` as the default value to indicate a parameter is required
196+
2. **Optional parameters**: Provide sensible defaults and mark as `Optional` in the type hint
197+
3. **Descriptions**: Write clear, informative descriptions for each parameter
198+
4. **Validation**: Use Field constraints like `ge`, `le`, `min_length`, `max_length`
199+
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`)
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/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()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ORACLE_LINUX_9_IMAGE = (
2+
"ocid1.image.oc1.iad.aaaaaaaa4l64brs5udx52nedrhlex4cpaorcd2jwvpoududksmw4lgmameqq"
3+
)
4+
E5_FLEX = "VM.Standard.E5.Flex"
5+
DEFAULT_OCPU_COUNT = 1
6+
DEFAULT_MEMORY_IN_GBS = 12

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
from datetime import datetime
8+
from enum import Enum
89
from typing import Any, Dict, List, Literal, Optional
910

1011
import oci
@@ -507,6 +508,22 @@ def map_instance(
507508
)
508509

509510

511+
class LifecycleState(str, Enum):
512+
"""
513+
LifecycleState options for an Instance
514+
"""
515+
516+
MOVING = "MOVING"
517+
PROVISIONING = "PROVISIONING"
518+
RUNNING = "RUNNING"
519+
STARTING = "STARTING"
520+
STOPPING = "STOPPING"
521+
STOPPED = "STOPPED"
522+
CREATING_IMAGE = "CREATING_IMAGE"
523+
TERMINATING = "TERMINATING"
524+
TERMINATED = "TERMINATED"
525+
526+
510527
# endregion
511528

512529
# region Image

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

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@
66

77
import os
88
from logging import Logger
9-
from typing import Annotated
9+
from typing import Annotated, Literal, Optional
1010

1111
import oci
1212
from fastmcp import FastMCP
13+
from oracle.oci_compute_mcp_server.consts import (
14+
DEFAULT_MEMORY_IN_GBS,
15+
DEFAULT_OCPU_COUNT,
16+
E5_FLEX,
17+
ORACLE_LINUX_9_IMAGE,
18+
)
1319
from oracle.oci_compute_mcp_server.models import (
1420
Image,
1521
Instance,
@@ -18,6 +24,7 @@
1824
map_instance,
1925
map_response,
2026
)
27+
from pydantic import Field
2128

2229
from . import __project__, __version__
2330

@@ -45,18 +52,25 @@ def get_compute_client():
4552

4653
@mcp.tool(description="List Instances in a given compartment")
4754
def list_instances(
48-
compartment_id: Annotated[str, "The OCID of the compartment"],
49-
limit: Annotated[
50-
int,
51-
"The maximum amount of instances to return. If None, there is no limit. "
52-
"If the value is not None, then it must be a positive number greater than 0.",
53-
] = None,
54-
lifecycle_state: Annotated[
55-
str,
56-
"The lifecycle state of the instance to filter on. The values can be: "
57-
"'MOVING', 'PROVISIONING', 'RUNNING', 'STARTING', 'STOPPING', 'STOPPED', "
58-
"'CREATING_IMAGE', 'TERMINATING', 'TERMINATED'",
59-
] = None,
55+
compartment_id: str = Field(..., description="The OCID of the compartment"),
56+
limit: Optional[int] = Field(
57+
None,
58+
description="The maximum amount of instances to return. If None, there is no limit.",
59+
ge=1,
60+
),
61+
lifecycle_state: Optional[
62+
Literal[
63+
"MOVING",
64+
"PROVISIONING",
65+
"RUNNING",
66+
"STARTING",
67+
"STOPPING",
68+
"STOPPED",
69+
"CREATING_IMAGE",
70+
"TERMINATING",
71+
"TERMINATED",
72+
]
73+
] = Field(None, description="The lifecycle state of the instance to filter on"),
6074
) -> list[Instance]:
6175
instances: list[Instance] = []
6276

@@ -109,14 +123,6 @@ def get_instance(instance_id: str) -> Instance:
109123
raise
110124

111125

112-
ORACLE_LINUX_9_IMAGE = (
113-
"ocid1.image.oc1.iad.aaaaaaaa4l64brs5udx52nedrhlex4cpaorcd2jwvpoududksmw4lgmameqq"
114-
)
115-
E5_FLEX = "VM.Standard.E5.Flex"
116-
DEFAULT_OCPU_COUNT = 1
117-
DEFAULT_MEMORY_IN_GBS = 12
118-
119-
120126
@mcp.tool(
121127
description="Create a new instance. "
122128
"Another word for instance could be compute, server, or virtual machine"

0 commit comments

Comments
 (0)