Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions projects/modelcontextprotocol/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright 2025 The OSS-Fuzz project authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# OSS-Fuzz integration for Model Context Protocol (MCP)
# Python/Atheris setup per OSS-Fuzz Python guide
FROM gcr.io/oss-fuzz-base/base-builder-python

# Install any additional Python deps needed by the fuzzer harness
# jsonschema is required by the harness to validate fuzzed JSON against MCP schema
RUN python3 -m pip install --no-cache-dir --upgrade pip \
&& python3 -m pip install --no-cache-dir jsonschema pyinstaller

# Fetch target repository (schemas and docs)
RUN git clone https://github.com/modelcontextprotocol/modelcontextprotocol.git $SRC/modelcontextprotocol

# Set workdir to repo (not strictly required, but convenient for builds)
WORKDIR $SRC/modelcontextprotocol

# Copy build script and fuzzers into $SRC for build step
COPY build.sh mcp_schema_fuzzer.py $SRC/
59 changes: 59 additions & 0 deletions projects/modelcontextprotocol/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/bin/bash -eu

# Copyright 2025 The OSS-Fuzz project authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Build script for OSS-Fuzz (Python/Atheris) project: modelcontextprotocol
# Reference: https://github.com/google/oss-fuzz/blob/master/docs/getting-started/new-project-guide/python_lang.md

# Locate the latest MCP JSON schema within the cloned repository.
# Expected structure: $SRC/modelcontextprotocol/schema/<date>/schema.json
SCHEMA_DIR=$(ls -d $SRC/modelcontextprotocol/schema/*/ 2>/dev/null | sort | tail -n1 || true)
SCHEMA_JSON=""
if [ -n "${SCHEMA_DIR}" ] && [ -f "${SCHEMA_DIR}/schema.json" ]; then
SCHEMA_JSON="${SCHEMA_DIR}/schema.json"
fi

# Build fuzzers into $OUT using PyInstaller for a hermetic binary.
# We assume all required Python deps were installed in the Dockerfile.
FUZZER_SRC="$SRC/mcp_schema_fuzzer.py"
FUZZER_NAME_PKG="mcp_schema_fuzzer.pkg"
FUZZER_WRAPPER_NAME="mcp_schema_fuzzer"

pyinstaller --distpath "$OUT" --onefile --name "$FUZZER_NAME_PKG" "$FUZZER_SRC"

# Provide schema.json next to the packaged binary if found; the harness prefers
# an adjacent schema.json, but also supports MCP_SCHEMA_PATH env var.
if [ -n "$SCHEMA_JSON" ]; then
cp "$SCHEMA_JSON" "$OUT/schema.json"
fi

# Create execution wrapper used by OSS-Fuzz
# Note: Because this fuzzer is pure-Python (no native extensions), do NOT set LD_PRELOAD.
cat > "$OUT/$FUZZER_WRAPPER_NAME" << 'EOF'
#!/bin/sh
# LLVMFuzzerTestOneInput for fuzzer detection.
this_dir=$(dirname "$0")
export MCP_SCHEMA_PATH="$this_dir/schema.json"
"$this_dir/mcp_schema_fuzzer.pkg" "$@"
EOF
chmod +x "$OUT/$FUZZER_WRAPPER_NAME"

# Optionally copy .options or .dict files if present in repository
if ls $SRC/*.options >/dev/null 2>&1; then
cp $SRC/*.options "$OUT/" || true
fi
if ls $SRC/*.dict >/dev/null 2>&1; then
cp $SRC/*.dict "$OUT/" || true
fi
120 changes: 120 additions & 0 deletions projects/modelcontextprotocol/mcp_schema_fuzzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""
Copyright 2025 The OSS-Fuzz project authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import atheris
import sys
import io
import os
import json
from typing import Optional

# We depend on jsonschema for validation; installed in Dockerfile.
try:
import jsonschema
from jsonschema.validators import validator_for
from jsonschema import FormatChecker
except Exception as e:
# If jsonschema isn't available, raise early during local runs.
raise


def _load_schema() -> Optional[dict]:
# Priority:
# 1) MCP_SCHEMA_PATH env var
# 2) schema.json adjacent to the executable (set by build.sh wrapper)
# 3) schema in the repo (useful for local runs)
candidates = []

env_path = os.environ.get("MCP_SCHEMA_PATH")
if env_path:
candidates.append(env_path)

# Adjacent to PyInstaller dist binary
exe_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
candidates.append(os.path.join(exe_dir, "schema.json"))

# Local repo fallback for manual testing
repo_schema_dir = os.path.join(os.environ.get("SRC", ""), "modelcontextprotocol", "schema")
if os.path.isdir(repo_schema_dir):
# Try to pick the latest subdir if present
try:
subdirs = [os.path.join(repo_schema_dir, d) for d in os.listdir(repo_schema_dir)]
subdirs = [d for d in subdirs if os.path.isdir(d)]
subdirs.sort()
if subdirs:
candidates.append(os.path.join(subdirs[-1], "schema.json"))
except Exception:
pass

for path in candidates:
try:
if path and os.path.isfile(path):
with open(path, "rb") as f:
return json.load(f)
except Exception:
# Ignore and try next candidate
continue
return None


SCHEMA = _load_schema()
VALIDATOR = None
if SCHEMA is not None:
try:
ValidatorClass = validator_for(SCHEMA)
ValidatorClass.check_schema(SCHEMA)
VALIDATOR = ValidatorClass(SCHEMA, format_checker=FormatChecker())
except Exception:
# If schema is invalid or unsupported, leave VALIDATOR as None.
VALIDATOR = None


def TestOneInput(data: bytes) -> None: # LLVMFuzzerTestOneInput name for libFuzzer
# Limit excessively large inputs to keep JSON decoding tractable
if len(data) > 1_000_000:
return

# Try UTF-8 first; if it fails, attempt latin-1 which maps bytes 1:1.
try:
s = data.decode("utf-8")
except UnicodeDecodeError:
try:
s = data.decode("latin-1")
except Exception:
return

# Attempt to parse JSON
try:
obj = json.loads(s)
except Exception:
return

# If we have a compiled validator, validate the object
if VALIDATOR is not None:
try:
VALIDATOR.validate(obj)
except Exception:
# We expect many validation failures; only crashes are interesting
pass


def main():
atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
atheris.Fuzz()


if __name__ == "__main__":
main()
25 changes: 25 additions & 0 deletions projects/modelcontextprotocol/project.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2025 The OSS-Fuzz project authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

homepage: https://modelcontextprotocol.io
main_repo: https://github.com/modelcontextprotocol/modelcontextprotocol.git
language: python3
fuzzing_engines:
- libfuzzer
sanitizers:
- address
- undefined
primary_contact: [email protected]
# Optional: help_url, issue filing, etc.
help_url: https://github.com/modelcontextprotocol/modelcontextprotocol/issues