Skip to content

Commit 1e6b57c

Browse files
committed
refactor to run with --mcp and add metrics
1 parent 4d45775 commit 1e6b57c

File tree

6 files changed

+115
-23
lines changed

6 files changed

+115
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
### Features
66

77
* **mcp:** Add Model Context Protocol (MCP) server support
8-
- New `--mcp-server` CLI option to start MCP server
8+
- New `--mcp` CLI option to start MCP server
99
- `ingest_repository` tool for LLM integration
1010
- Full MCP protocol compliance with stdio transport
1111
- Enhanced MCP client examples for stdio transport

examples/mcp-config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"mcpServers": {
33
"gitingest": {
44
"command": "gitingest",
5-
"args": ["--mcp-server"],
5+
"args": ["--mcp"],
66
"env": {
77
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
88
}

src/gitingest/__main__.py

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from __future__ import annotations
55

66
import asyncio
7+
import os
78
from typing import TypedDict
89

910
import click
@@ -15,6 +16,15 @@
1516
# Import logging configuration first to intercept all logging
1617
from gitingest.utils.logging_config import get_logger
1718

19+
# Optional MCP imports
20+
try:
21+
from gitingest.mcp_server import start_mcp_server
22+
from mcp_server.main import start_mcp_server_tcp
23+
24+
MCP_AVAILABLE = True
25+
except ImportError:
26+
MCP_AVAILABLE = False
27+
1828
# Initialize logger for this module
1929
logger = get_logger(__name__)
2030

@@ -29,7 +39,10 @@ class _CLIArgs(TypedDict):
2939
include_submodules: bool
3040
token: str | None
3141
output: str | None
32-
mcp_server: bool
42+
mcp: bool
43+
transport: str
44+
host: str
45+
port: int
3346

3447

3548
@click.command()
@@ -78,11 +91,31 @@ class _CLIArgs(TypedDict):
7891
help="Output file path (default: digest.txt in current directory). Use '-' for stdout.",
7992
)
8093
@click.option(
81-
"--mcp-server",
94+
"--mcp",
8295
is_flag=True,
8396
default=False,
8497
help="Start the MCP (Model Context Protocol) server for LLM integration",
8598
)
99+
@click.option(
100+
"--transport",
101+
type=click.Choice(["stdio", "tcp"]),
102+
default="stdio",
103+
show_default=True,
104+
help="Transport protocol for MCP communication (only used with --mcp)",
105+
)
106+
@click.option(
107+
"--host",
108+
default="127.0.0.1",
109+
show_default=True,
110+
help="Host to bind TCP server (only used with --mcp --transport tcp)",
111+
)
112+
@click.option(
113+
"--port",
114+
type=int,
115+
default=8001,
116+
show_default=True,
117+
help="Port for TCP server (only used with --mcp --transport tcp)",
118+
)
86119
def main(**cli_kwargs: Unpack[_CLIArgs]) -> None:
87120
"""Run the CLI entry point to analyze a repo / directory and dump its contents.
88121
@@ -107,7 +140,8 @@ def main(**cli_kwargs: Unpack[_CLIArgs]) -> None:
107140
$ gitingest https://github.com/user/repo --output -
108141
109142
MCP server mode:
110-
$ gitingest --mcp-server
143+
$ gitingest --mcp
144+
$ gitingest --mcp --transport tcp --host 0.0.0.0 --port 8001
111145
112146
With filtering:
113147
$ gitingest -i "*.py" -e "*.log"
@@ -135,7 +169,10 @@ async def _async_main(
135169
include_submodules: bool = False,
136170
token: str | None = None,
137171
output: str | None = None,
138-
mcp_server: bool = False,
172+
mcp: bool = False,
173+
transport: str = "stdio",
174+
host: str = "127.0.0.1",
175+
port: int = 8001,
139176
) -> None:
140177
"""Analyze a directory or repository and create a text dump of its contents.
141178
@@ -165,8 +202,14 @@ async def _async_main(
165202
output : str | None
166203
The path where the output file will be written (default: ``digest.txt`` in current directory).
167204
Use ``"-"`` to write to ``stdout``.
168-
mcp_server : bool
205+
mcp : bool
169206
If ``True``, starts the MCP (Model Context Protocol) server instead of normal operation (default: ``False``).
207+
transport : str
208+
Transport protocol for MCP communication: "stdio" or "tcp" (default: "stdio").
209+
host : str
210+
Host to bind TCP server (only used with transport="tcp", default: "127.0.0.1").
211+
port : int
212+
Port for TCP server (only used with transport="tcp", default: 8001).
170213
171214
Raises
172215
------
@@ -177,17 +220,21 @@ async def _async_main(
177220
178221
"""
179222
# Check if MCP server mode is requested
180-
if mcp_server:
181-
# Dynamic import to avoid circular imports and optional dependency
182-
try:
183-
from gitingest.mcp_server import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel
184-
start_mcp_server,
185-
)
186-
223+
if mcp:
224+
if not MCP_AVAILABLE:
225+
msg = "MCP server dependencies not installed"
226+
raise click.ClickException(msg)
227+
228+
if transport == "tcp":
229+
# Use TCP transport with FastMCP and metrics support
230+
# Enable metrics for TCP mode if not already set
231+
if os.getenv("GITINGEST_METRICS_ENABLED") is None:
232+
os.environ["GITINGEST_METRICS_ENABLED"] = "true"
233+
234+
await start_mcp_server_tcp(host, port)
235+
else:
236+
# Use stdio transport (default) - metrics not available in stdio mode
187237
await start_mcp_server()
188-
except ImportError as e:
189-
msg = f"MCP server dependencies not installed: {e}"
190-
raise click.ClickException(msg) from e
191238
return
192239

193240
try:

src/gitingest/mcp_server.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from mcp.server import Server # pylint: disable=import-error
88
from mcp.server.stdio import stdio_server # pylint: disable=import-error
99
from mcp.types import TextContent, Tool # pylint: disable=import-error
10+
from prometheus_client import Counter
1011

1112
from gitingest.entrypoint import ingest_async
1213
from gitingest.utils.logging_config import get_logger
@@ -17,6 +18,10 @@
1718
# Initialize logger for this module
1819
logger = get_logger(__name__)
1920

21+
# Create Prometheus metrics
22+
mcp_ingest_counter = Counter("gitingest_mcp_ingest_total", "Number of MCP ingests", ["status"])
23+
mcp_tool_calls_counter = Counter("gitingest_mcp_tool_calls_total", "Number of MCP tool calls", ["tool_name", "status"])
24+
2025
# Create the MCP server instance
2126
app = Server("gitingest")
2227

@@ -84,11 +89,18 @@ async def list_tools() -> list[Tool]:
8489
async def call_tool(name: str, arguments: dict[str, Any]) -> Sequence[TextContent]:
8590
"""Execute a tool."""
8691
try:
92+
mcp_tool_calls_counter.labels(tool_name=name, status="started").inc()
93+
8794
if name == "ingest_repository":
88-
return await _handle_ingest_repository(arguments)
95+
result = await _handle_ingest_repository(arguments)
96+
mcp_tool_calls_counter.labels(tool_name=name, status="success").inc()
97+
return result
98+
99+
mcp_tool_calls_counter.labels(tool_name=name, status="unknown_tool").inc()
89100
return [TextContent(type="text", text=f"Unknown tool: {name}")]
90101
except Exception as e:
91102
logger.exception("Error in tool call %s", name)
103+
mcp_tool_calls_counter.labels(tool_name=name, status="error").inc()
92104
return [TextContent(type="text", text=f"Error executing {name}: {e!s}")]
93105

94106

@@ -143,10 +155,12 @@ async def _handle_ingest_repository(arguments: dict[str, Any]) -> Sequence[TextC
143155
*Generated by Gitingest MCP Server*
144156
"""
145157

158+
mcp_ingest_counter.labels(status="success").inc()
146159
return [TextContent(type="text", text=response_content)]
147160

148161
except Exception as e:
149162
logger.exception("Error during ingestion")
163+
mcp_ingest_counter.labels(status="error").inc()
150164
return [TextContent(type="text", text=f"Error ingesting repository: {e!s}")]
151165

152166

src/mcp_server/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
@click.option(
2929
"--port",
3030
type=int,
31-
default=8001,
31+
default=8000,
3232
show_default=True,
3333
help="Port for TCP server (only used with --transport tcp)",
3434
)

src/mcp_server/main.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,31 @@
22

33
from __future__ import annotations
44

5+
import os
6+
import threading
7+
8+
import uvicorn
9+
from fastapi import FastAPI
10+
from fastapi.middleware.cors import CORSMiddleware
11+
from fastapi.responses import JSONResponse
512
from mcp.server.fastmcp import FastMCP # pylint: disable=import-error
13+
from prometheus_client import Counter
614

715
from gitingest.entrypoint import ingest_async
816
from gitingest.utils.logging_config import get_logger
17+
from server.metrics_server import start_metrics_server
918

1019
# Initialize logger for this module
1120
logger = get_logger(__name__)
1221

22+
# Create Prometheus metrics
23+
fastmcp_ingest_counter = Counter("gitingest_fastmcp_ingest_total", "Number of FastMCP ingests", ["status"])
24+
fastmcp_tool_calls_counter = Counter(
25+
"gitingest_fastmcp_tool_calls_total",
26+
"Number of FastMCP tool calls",
27+
["tool_name", "status"],
28+
)
29+
1330
# Create the FastMCP server instance
1431
mcp = FastMCP("gitingest")
1532

@@ -40,6 +57,7 @@ async def ingest_repository(
4057
4158
"""
4259
try:
60+
fastmcp_tool_calls_counter.labels(tool_name="ingest_repository", status="started").inc()
4361
logger.info("Starting MCP ingestion", extra={"source": source})
4462

4563
# Convert patterns to sets if provided
@@ -58,8 +76,14 @@ async def ingest_repository(
5876
token=token,
5977
output=None, # Don't write to file, return content instead
6078
)
79+
80+
fastmcp_ingest_counter.labels(status="success").inc()
81+
fastmcp_tool_calls_counter.labels(tool_name="ingest_repository", status="success").inc()
82+
6183
except Exception:
6284
logger.exception("Error during ingestion")
85+
fastmcp_ingest_counter.labels(status="error").inc()
86+
fastmcp_tool_calls_counter.labels(tool_name="ingest_repository", status="error").inc()
6387
return "Error ingesting repository: An internal error occurred"
6488

6589
# Create a structured response and return directly
@@ -85,10 +109,17 @@ async def start_mcp_server_tcp(host: str = "127.0.0.1", port: int = 8001) -> Non
85109
"""Start the MCP server with HTTP transport using SSE."""
86110
logger.info("Starting Gitingest MCP server with HTTP/SSE transport on %s:%s", host, port)
87111

88-
import uvicorn # noqa: PLC0415 # pylint: disable=import-outside-toplevel
89-
from fastapi import FastAPI # noqa: PLC0415 # pylint: disable=import-outside-toplevel
90-
from fastapi.middleware.cors import CORSMiddleware # noqa: PLC0415 # pylint: disable=import-outside-toplevel
91-
from fastapi.responses import JSONResponse # noqa: PLC0415 # pylint: disable=import-outside-toplevel
112+
# Start metrics server in a separate thread if enabled
113+
if os.getenv("GITINGEST_METRICS_ENABLED") is not None:
114+
metrics_host = os.getenv("GITINGEST_METRICS_HOST", "127.0.0.1")
115+
metrics_port = int(os.getenv("GITINGEST_METRICS_PORT", "9090"))
116+
metrics_thread = threading.Thread(
117+
target=start_metrics_server,
118+
args=(metrics_host, metrics_port),
119+
daemon=True,
120+
)
121+
metrics_thread.start()
122+
logger.info("Started metrics server on %s:%s", metrics_host, metrics_port)
92123

93124
tcp_app = FastAPI(title="Gitingest MCP Server", description="MCP server over HTTP/SSE")
94125

0 commit comments

Comments
 (0)