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
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: Create for specific functionality (e.g., API clients)
General Principles
- Keep modules focused and limited to a single responsibility
- Use clear and consistent naming conventions
- Follow Python’s PEP 8 style guide
Entry Points
MCP servers should follow these guidelines for application entry points:
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
Main Function
Implement a main() function in server.py that:
- Handles command-line arguments
- Sets up environment and logging
- Initializes the MCP server
Example:
def main():
"""Run the MCP server with CLI argument support."""
mcp.run()
if __name__ == '__main__':
main()
Package Entry Point
Configure the entry point in pyproject.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:
"""
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
- Make all models Pydantic: This ensures serializability. You may refer to the OCI Python SDK for reference to most OCI models.
- Define Literals for constrained values: Use
Literal types for parameters with fixed valid values.
- Add comprehensive descriptions to each field: Clear descriptions help both developers and AI assistants understand the purpose.
Pydantic Model Example
Here’s an example Pydantic model for NetworkSecurityGroup:
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."
)
You can use AI assistants like Cline to generate Pydantic models from OCI SDK models. Provide a prompt like:“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.
Example from list_instances:
@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
- Required parameters: Use
... as the default value to indicate a parameter is required
- Optional parameters: Provide sensible defaults and mark as
Optional in the type hint
- Descriptions: Write clear, informative descriptions for each parameter
- Validation: Use Field constraints like
ge, le, min_length, max_length
- Literals: Use
Literal for parameters with a fixed set of valid values
Error Handling
- Always wrap external API calls in try-except blocks
- Log errors with appropriate context
- Re-raise exceptions to allow the MCP framework to handle them properly
- Provide meaningful error messages to users