This guide walks you through creating a new MCP server in the repository, following established patterns and best practices.
Prerequisites
Before creating a new MCP server, ensure you have:
- Python 3.13 or later installed
uv package manager installed
- Basic understanding of the Model Context Protocol (MCP)
- Familiarity with Pydantic models
- Access to relevant API documentation (e.g., OCI SDK docs)
Step 1: Create Directory Structure
Create the directory structure under src/:
cd src
mkdir -p my-service-mcp-server/oracle/my_service_mcp_server/tests
Use hyphens in the directory name (my-service-mcp-server) but underscores in the Python package name (my_service_mcp_server).
Step 2: Create pyproject.toml
Create src/my-service-mcp-server/pyproject.toml:
[project]
name = "oracle.my-service-mcp-server"
version = "1.0.0"
description = "My Service MCP server"
readme = "README.md"
requires-python = ">=3.13"
license = "UPL-1.0"
license-files = ["LICENSE.txt"]
authors = [
{name = "Oracle MCP", email = "[email protected]"},
]
dependencies = [
"fastmcp==2.14.2",
"oci==2.160.0",
"pydantic==2.12.3",
]
classifiers = [
"License :: OSI Approved :: Universal Permissive License (UPL)",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.13",
]
[project.scripts]
"oracle.my-service-mcp-server" = "oracle.my_service_mcp_server.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["oracle"]
[dependency-groups]
dev = [
"pytest>=8.4.2",
"pytest-asyncio>=1.2.0",
"pytest-cov>=7.0.0",
]
[tool.coverage.run]
omit = [
"**/__init__.py",
"**/tests/*",
"dist/*",
".venv/*",
]
[tool.coverage.report]
omit = [
"**/__init__.py",
"**/tests/*",
]
precision = 2
fail_under = 90
Step 3: Create Package Files
Create __init__.py
Create src/my-service-mcp-server/oracle/__init__.py (empty file):
touch src/my-service-mcp-server/oracle/__init__.py
Create src/my-service-mcp-server/oracle/my_service_mcp_server/__init__.py:
"""
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.
"""
__project__ = "oracle.my-service-mcp-server"
__version__ = "1.0.0"
Create consts.py
Create src/my-service-mcp-server/oracle/my_service_mcp_server/consts.py:
"""
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.
"""
# Define your constants here
DEFAULT_TIMEOUT = 30
MAX_RETRIES = 3
Create models.py
Create src/my-service-mcp-server/oracle/my_service_mcp_server/models.py:
"""
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.
"""
from typing import Optional, Literal
from datetime import datetime
from pydantic import BaseModel, Field
# Define your Pydantic models here
class MyResource(BaseModel):
"""
Pydantic model for MyResource.
"""
id: Optional[str] = Field(
None,
description="The OCID of the resource."
)
display_name: Optional[str] = Field(
None,
description="A user-friendly name."
)
compartment_id: Optional[str] = Field(
None,
description="The OCID of the compartment."
)
lifecycle_state: Optional[Literal[
"CREATING",
"ACTIVE",
"DELETING",
"DELETED"
]] = Field(
None,
description="The current lifecycle state."
)
time_created: Optional[datetime] = Field(
None,
description="The date and time the resource was created."
)
def map_my_resource(oci_resource) -> MyResource:
"""
Map OCI SDK model to Pydantic model.
"""
return MyResource(
id=oci_resource.id,
display_name=oci_resource.display_name,
compartment_id=oci_resource.compartment_id,
lifecycle_state=oci_resource.lifecycle_state,
time_created=oci_resource.time_created
)
Step 4: Create Server Implementation
Create src/my-service-mcp-server/oracle/my_service_mcp_server/server.py:
"""
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.
"""
import os
from logging import Logger
from typing import Optional
import oci
from fastmcp import FastMCP
from pydantic import Field
from . import __project__, __version__
from .models import MyResource, map_my_resource
from .consts import DEFAULT_TIMEOUT
logger = Logger(__name__, level="INFO")
mcp = FastMCP(name=__project__)
def get_service_client():
"""Initialize and return the OCI service client."""
logger.info("Initializing service client")
config = oci.config.from_file(
file_location=os.getenv("OCI_CONFIG_FILE", oci.config.DEFAULT_LOCATION),
profile_name=os.getenv("OCI_CONFIG_PROFILE", oci.config.DEFAULT_PROFILE),
)
user_agent_name = __project__.split("oracle.", 1)[1].split("-server", 1)[0]
config["additional_user_agent"] = f"{user_agent_name}/{__version__}"
# Handle security token authentication
private_key = oci.signer.load_private_key_from_file(config["key_file"])
token_file = os.path.expanduser(config["security_token_file"])
with open(token_file, "r") as f:
token = f.read()
signer = oci.auth.signers.SecurityTokenSigner(token, private_key)
# Replace with your actual OCI client
# return oci.my_service.MyServiceClient(config, signer=signer)
return None
@mcp.tool(description="List resources in a given compartment")
def list_resources(
compartment_id: str = Field(..., description="The OCID of the compartment"),
limit: Optional[int] = Field(
None,
description="The maximum number of resources to return.",
ge=1,
),
) -> list[MyResource]:
"""List resources in the specified compartment."""
resources: list[MyResource] = []
try:
client = get_service_client()
response = None
has_next_page = True
next_page: str = None
while has_next_page and (limit is None or len(resources) < limit):
kwargs = {
"compartment_id": compartment_id,
"page": next_page,
"limit": limit,
}
# Replace with your actual API call
# response = client.list_resources(**kwargs)
# has_next_page = response.has_next_page
# next_page = response.next_page if hasattr(response, "next_page") else None
# data = response.data
# for d in data:
# resource = map_my_resource(d)
# resources.append(resource)
logger.info(f"Found {len(resources)} resources")
return resources
except Exception as e:
logger.error(f"Error in list_resources tool: {str(e)}")
raise e
@mcp.tool(description="Get a resource by ID")
def get_resource(
resource_id: str = Field(..., description="The OCID of the resource"),
) -> MyResource:
"""Get details of a specific resource."""
try:
client = get_service_client()
# Replace with your actual API call
# response = client.get_resource(resource_id)
# return map_my_resource(response.data)
logger.info(f"Retrieved resource {resource_id}")
return None
except Exception as e:
logger.error(f"Error in get_resource tool: {str(e)}")
raise e
def main():
"""Run the MCP server."""
mcp.run()
if __name__ == '__main__':
main()
Step 5: Create README
Create src/my-service-mcp-server/README.md:
# My Service MCP Server
MCP server for interacting with My Service.
## Features
- List resources in a compartment
- Get resource details
- [Add more features as you implement them]
## Installation
```bash
uvx oracle.my-service-mcp-server@latest
Configuration
See the main repository README for authentication setup.
list_resources
List resources in a given compartment.
get_resource
Get details of a specific resource.
## Step 6: Copy LICENSE.txt
Copy the LICENSE.txt file from another server:
```bash
cp src/oci-compute-mcp-server/LICENSE.txt src/my-service-mcp-server/
Step 7: Build and Test
Lock Dependencies
cd src/my-service-mcp-server
uv lock
Build the Package
cd ../..
make build SUBDIRS=src/my-service-mcp-server
Install Locally
make install SUBDIRS=src/my-service-mcp-server
Test with Inspector
npx @modelcontextprotocol/inspector \
uv \
--directory $(pwd)/src/my-service-mcp-server/oracle/my_service_mcp_server \
run \
server.py
Step 8: Write Tests
Create test files in src/my-service-mcp-server/oracle/my_service_mcp_server/tests/:
"""
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.
"""
import pytest
from unittest.mock import Mock, patch
from oracle.my_service_mcp_server.server import list_resources
class TestListResources:
@patch('oracle.my_service_mcp_server.server.get_service_client')
def test_list_resources_success(self, mock_client):
# Arrange
mock_response = Mock(
data=[Mock(id="ocid1.resource.test")],
has_next_page=False
)
mock_client.return_value.list_resources.return_value = mock_response
# Act
result = list_resources(compartment_id="ocid1.compartment.test")
# Assert
assert len(result) >= 0
Step 9: Add to Makefile
The Makefile will automatically pick up your new server if itβs in the src/ directory and has a pyproject.toml file.
Best Practices Checklist
Reference Servers
Look at these servers as examples:
- Simple server:
oci-compute-mcp-server
- Complex server:
oci-networking-mcp-server
- API wrapper:
oci-api-mcp-server
Next Steps
After creating your server:
- Test thoroughly with MCP Inspector
- Write comprehensive tests
- Update the main repository README to include your server
- Follow the contributing guide to submit your changes
- Consider adding to the publishing workflow if appropriate