Skip to content

Commit f484a18

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

File tree

6 files changed

+111
-19
lines changed

6 files changed

+111
-19
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: 58 additions & 11 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
@@ -29,7 +30,10 @@ class _CLIArgs(TypedDict):
2930
include_submodules: bool
3031
token: str | None
3132
output: str | None
32-
mcp_server: bool
33+
mcp: bool
34+
transport: str
35+
host: str
36+
port: int
3337

3438

3539
@click.command()
@@ -78,11 +82,31 @@ class _CLIArgs(TypedDict):
7882
help="Output file path (default: digest.txt in current directory). Use '-' for stdout.",
7983
)
8084
@click.option(
81-
"--mcp-server",
85+
"--mcp",
8286
is_flag=True,
8387
default=False,
8488
help="Start the MCP (Model Context Protocol) server for LLM integration",
8589
)
90+
@click.option(
91+
"--transport",
92+
type=click.Choice(["stdio", "tcp"]),
93+
default="stdio",
94+
show_default=True,
95+
help="Transport protocol for MCP communication (only used with --mcp)",
96+
)
97+
@click.option(
98+
"--host",
99+
default="127.0.0.1",
100+
show_default=True,
101+
help="Host to bind TCP server (only used with --mcp --transport tcp)",
102+
)
103+
@click.option(
104+
"--port",
105+
type=int,
106+
default=8001,
107+
show_default=True,
108+
help="Port for TCP server (only used with --mcp --transport tcp)",
109+
)
86110
def main(**cli_kwargs: Unpack[_CLIArgs]) -> None:
87111
"""Run the CLI entry point to analyze a repo / directory and dump its contents.
88112
@@ -107,7 +131,8 @@ def main(**cli_kwargs: Unpack[_CLIArgs]) -> None:
107131
$ gitingest https://github.com/user/repo --output -
108132
109133
MCP server mode:
110-
$ gitingest --mcp-server
134+
$ gitingest --mcp
135+
$ gitingest --mcp --transport tcp --host 0.0.0.0 --port 8001
111136
112137
With filtering:
113138
$ gitingest -i "*.py" -e "*.log"
@@ -135,7 +160,10 @@ async def _async_main(
135160
include_submodules: bool = False,
136161
token: str | None = None,
137162
output: str | None = None,
138-
mcp_server: bool = False,
163+
mcp: bool = False,
164+
transport: str = "stdio",
165+
host: str = "127.0.0.1",
166+
port: int = 8001,
139167
) -> None:
140168
"""Analyze a directory or repository and create a text dump of its contents.
141169
@@ -165,8 +193,14 @@ async def _async_main(
165193
output : str | None
166194
The path where the output file will be written (default: ``digest.txt`` in current directory).
167195
Use ``"-"`` to write to ``stdout``.
168-
mcp_server : bool
196+
mcp : bool
169197
If ``True``, starts the MCP (Model Context Protocol) server instead of normal operation (default: ``False``).
198+
transport : str
199+
Transport protocol for MCP communication: "stdio" or "tcp" (default: "stdio").
200+
host : str
201+
Host to bind TCP server (only used with transport="tcp", default: "127.0.0.1").
202+
port : int
203+
Port for TCP server (only used with transport="tcp", default: 8001).
170204
171205
Raises
172206
------
@@ -177,14 +211,27 @@ async def _async_main(
177211
178212
"""
179213
# Check if MCP server mode is requested
180-
if mcp_server:
214+
if mcp:
181215
# Dynamic import to avoid circular imports and optional dependency
182216
try:
183-
from gitingest.mcp_server import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel
184-
start_mcp_server,
185-
)
186-
187-
await start_mcp_server()
217+
if transport == "tcp":
218+
# Use TCP transport with FastMCP and metrics support
219+
# Enable metrics for TCP mode if not already set
220+
if os.getenv("GITINGEST_METRICS_ENABLED") is None:
221+
os.environ["GITINGEST_METRICS_ENABLED"] = "true"
222+
223+
from mcp_server.main import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
224+
start_mcp_server_tcp,
225+
)
226+
227+
await start_mcp_server_tcp(host, port)
228+
else:
229+
# Use stdio transport (default) - metrics not available in stdio mode
230+
from gitingest.mcp_server import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
231+
start_mcp_server,
232+
)
233+
234+
await start_mcp_server()
188235
except ImportError as e:
189236
msg = f"MCP server dependencies not installed: {e}"
190237
raise click.ClickException(msg) from e

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)