# 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 | [](https://www.python.org/)
4 | [](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 | 
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 |
```