#
tokens: 2582/50000 5/5 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── docs
│   └── mcp-inspector.png
├── pyproject.toml
├── README.md
└── src
    └── mcp_sbom
        ├── __init__.py
        ├── sbom.json
        └── server.py
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Byte-compiled / optimized / DLL files
  2 | __pycache__/
  3 | *.py[cod]
  4 | *$py.class
  5 | 
  6 | # C extensions
  7 | *.so
  8 | 
  9 | # Distribution / packaging
 10 | .Python
 11 | build/
 12 | develop-eggs/
 13 | dist/
 14 | downloads/
 15 | eggs/
 16 | .eggs/
 17 | lib/
 18 | lib64/
 19 | parts/
 20 | sdist/
 21 | var/
 22 | wheels/
 23 | share/python-wheels/
 24 | *.egg-info/
 25 | .installed.cfg
 26 | *.egg
 27 | MANIFEST
 28 | 
 29 | # PyInstaller
 30 | #  Usually these files are written by a python script from a template
 31 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
 32 | *.manifest
 33 | *.spec
 34 | 
 35 | # Installer logs
 36 | pip-log.txt
 37 | pip-delete-this-directory.txt
 38 | 
 39 | # Unit test / coverage reports
 40 | htmlcov/
 41 | .tox/
 42 | .nox/
 43 | .coverage
 44 | .coverage.*
 45 | .cache
 46 | nosetests.xml
 47 | coverage.xml
 48 | *.cover
 49 | *.py,cover
 50 | .hypothesis/
 51 | .pytest_cache/
 52 | cover/
 53 | 
 54 | # Translations
 55 | *.mo
 56 | *.pot
 57 | 
 58 | # Django stuff:
 59 | *.log
 60 | local_settings.py
 61 | db.sqlite3
 62 | db.sqlite3-journal
 63 | 
 64 | # Flask stuff:
 65 | instance/
 66 | .webassets-cache
 67 | 
 68 | # Scrapy stuff:
 69 | .scrapy
 70 | 
 71 | # Sphinx documentation
 72 | docs/_build/
 73 | 
 74 | # PyBuilder
 75 | .pybuilder/
 76 | target/
 77 | 
 78 | # Jupyter Notebook
 79 | .ipynb_checkpoints
 80 | 
 81 | # IPython
 82 | profile_default/
 83 | ipython_config.py
 84 | 
 85 | .python-version
 86 | 
 87 | # UV
 88 | uv.lock
 89 | 
 90 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
 91 | __pypackages__/
 92 | 
 93 | # Celery stuff
 94 | celerybeat-schedule
 95 | celerybeat.pid
 96 | 
 97 | # SageMath parsed files
 98 | *.sage.py
 99 | 
100 | # Environments
101 | .env
102 | .venv
103 | env/
104 | venv/
105 | ENV/
106 | env.bak/
107 | venv.bak/
108 | 
109 | # Spyder project settings
110 | .spyderproject
111 | .spyproject
112 | 
113 | # Rope project settings
114 | .ropeproject
115 | 
116 | # mkdocs documentation
117 | /site
118 | 
119 | # mypy
120 | .mypy_cache/
121 | .dmypy.json
122 | dmypy.json
123 | 
124 | # Pyre type checker
125 | .pyre/
126 | 
127 | # pytype static type analyzer
128 | .pytype/
129 | 
130 | # Cython debug symbols
131 | cython_debug/
132 | 
133 | # PyCharm
134 | #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
135 | #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
136 | #  and can be added to the global gitignore or merged into this file.  For a more nuclear
137 | #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
138 | #.idea/
139 | 
140 | # Ruff stuff:
141 | .ruff_cache/
142 | 
143 | # PyPI configuration file
144 | .pypirc
145 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # MCP SBOM Server
 2 | 
 3 | [![Python](https://img.shields.io/badge/Python-3.12-blue.svg)](https://www.python.org/)
 4 | [![MCP](https://img.shields.io/badge/MCP-1.6-CC5500.svg)](https://www.anthropic.com/news/model-context-protocol)
 5 | 
 6 | MCP server to perform a Trivy scan and produce an SBOM in CycloneDX format.
 7 | 
 8 | ## Installation
 9 | 
10 | ### Prerequisites
11 | 
12 | Install the following.
13 | 
14 | - [uv](https://github.com/astral-sh/uv)
15 | - [trivy](https://github.com/aquasecurity/trivy)
16 | - [Node.js](https://nodejs.org/en)
17 | 
18 | ## MCP Clients
19 | 
20 | ### Configuration
21 | 
22 | ```json
23 | "mcpServers": {
24 |         "mcp-sbom": {
25 |             "command": "uv",
26 |             "args": [
27 |                 "--directory",
28 |                 "/path/to/mcp-sbom",
29 |                 "run",
30 |                 "mcp-sbom"
31 |             ]
32 |         }
33 |     }
34 | ```
35 | 
36 | ## Building
37 | 
38 | > [!NOTE]
39 | > This project employs `uv`.
40 | 
41 | 1. Synchronize dependencies and update the lockfile.
42 | ```
43 | uv sync
44 | ```
45 | 
46 | ## Debugging
47 | 
48 | ### MCP Inspector
49 | 
50 | Use [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
51 | 
52 | Launch the MCP Inspector as follows:
53 | 
54 | ```
55 | npx @modelcontextprotocol/inspector uv --directory /path/to/mcp-sbom run mcp-sbom
56 | ```
57 | 
58 | ![MCP Inspector](docs/mcp-inspector.png)
59 | 
60 | ### Windows
61 | 
62 | When running on Windows, use paths of the style:
63 | 
64 | ```console
65 | C:/Users/gkh/src/mcp-sbom-server/src/mcp_sbom
66 | ```
67 | 
```

--------------------------------------------------------------------------------
/src/mcp_sbom/__init__.py:
--------------------------------------------------------------------------------

```python
1 | from . import server
2 | import asyncio
3 | 
4 | def main():
5 |     """Main entry point for the package."""
6 |     asyncio.run(server.main())
7 | 
8 | __all__ = [ 'main', 'server' ]
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "mcp-sbom"
 3 | version = "0.1.0"
 4 | description = "MCP server to perform a scan and produce an SBOM"
 5 | readme = "README.md"
 6 | requires-python = ">=3.12"
 7 | dependencies = [
 8 |     "mcp[cli]>=1.6.0",
 9 |     "python-dotenv>=1.0.1",
10 | ]
11 | 
12 | [build-system]
13 | requires = ["hatchling"]
14 | build-backend = "hatchling.build"
15 | 
16 | [dependency-groups]
17 | dev = [
18 |     "pyright>=1.1.389",
19 | ]
20 | 
21 | [project.scripts]
22 | mcp-sbom = "mcp_sbom:main"
```

--------------------------------------------------------------------------------
/src/mcp_sbom/server.py:
--------------------------------------------------------------------------------

```python
 1 | import asyncio
 2 | import json
 3 | import logging
 4 | from mcp.server.fastmcp import FastMCP
 5 | 
 6 | logging.basicConfig(
 7 |     level=logging.DEBUG,
 8 |     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
 9 | )
10 | logger = logging.getLogger("mcp-sbom")
11 | 
12 | mcp = FastMCP("mcp-sbom")
13 | 
14 | async def exec_trivy(image: str):
15 |     try:
16 |         logger.info(f"Starting Trivy scan for image: {image}")
17 |         cmd = [
18 |             "trivy", "image", 
19 |             "--format", "cyclonedx", 
20 |             "--output", "sbom.json", 
21 |             image
22 |             ]
23 |         # result = subprocess.run(cmd, capture_output=True, text=True)
24 |         process = await asyncio.create_subprocess_exec(
25 |             *cmd, 
26 |             stdout=asyncio.subprocess.PIPE, 
27 |             stderr=asyncio.subprocess.PIPE
28 |             )
29 |         stdout, stderr = await process.communicate()
30 |         logger.info(f"Trivy scan completed with return code {process.returncode}")
31 |         
32 |         if process.returncode == 0:
33 |             with open("sbom.json", "r") as f:
34 |                 sbom_content = json.load(f)
35 |         return sbom_content
36 |     except Exception as e:
37 |         logger.error(f"Exception in exec_trivy: {str(e)}")
38 |         return f"Error: {str(e)}"
39 | 
40 | @mcp.tool()
41 | async def scan(image: str):
42 |     """
43 |     Execute Trivy scanner to generate SPDX SBOM for a container image.
44 |     Supports the SPDX JSON format.
45 | 
46 |     Args:
47 |         image (str): The container image name/reference to scan
48 | 
49 |     Returns:
50 |         str: Test response or error message
51 |     """
52 |     try:
53 |         logger.info(f"MCP SBOM tool called with image: {image}")
54 |         result = await exec_trivy(image)
55 |         logger.debug(f"Trivy execution result: {result}")
56 |         return result
57 |     except Exception as e:
58 |         logger.error(f"Exception in trivy tool: {str(e)}")
59 |         return f"Error: {str(e)}"
60 | 
61 | # if __name__ == "__main__":
62 | def main():
63 |     logger.info("Starting SBOM MCP Server!")
64 | 
65 |     try:
66 |         mcp.run(transport="stdio")
67 |     except Exception as e:
68 |         logger.error(f"Error running MCP server: {str(e)}")
69 | 
```