Skip to main content
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.

Tools

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

  • Follow the directory structure convention
  • Include copyright headers in all Python files
  • Use Pydantic models for all data structures
  • Add comprehensive Field descriptions
  • Implement proper error handling
  • Write unit tests with good coverage
  • Document all tools in README
  • Use meaningful logging
  • Follow naming conventions (hyphens vs underscores)
  • Test with MCP Inspector before committing

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:
  1. Test thoroughly with MCP Inspector
  2. Write comprehensive tests
  3. Update the main repository README to include your server
  4. Follow the contributing guide to submit your changes
  5. Consider adding to the publishing workflow if appropriate

Build docs developers (and LLMs) love