Skip to content

Commit a1d28a8

Browse files
authored
chore: refactor mcp
feat: refactor mcp and move to new dir
2 parents 00cbea4 + 5742ce4 commit a1d28a8

File tree

37 files changed

+2748
-1340
lines changed

37 files changed

+2748
-1340
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
OpenTelemetry MCP Instrumentation
2+
==================================
3+
4+
|pypi|
5+
6+
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-mcp.svg
7+
:target: https://pypi.org/project/opentelemetry-instrumentation-mcp/
8+
9+
This library allows tracing MCP (Model Context Protocol) client requests made by the
10+
`mcp <https://pypi.org/project/mcp/>`_ library.
11+
12+
Installation
13+
------------
14+
15+
::
16+
17+
pip install opentelemetry-instrumentation-mcp
18+
19+
20+
To install the instrumentation along with the target library, run:
21+
22+
::
23+
24+
pip install opentelemetry-instrumentation-mcp[instruments]
25+
26+
27+
References
28+
----------
29+
30+
* `OpenTelemetry MCP Instrumentation <https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/mcp/mcp.html>`_
31+
* `OpenTelemetry Project <https://opentelemetry.io/>`_
32+
* `MCP (Model Context Protocol) <https://modelcontextprotocol.io/>`_

opentelemetry-instrumentation/example/mcp/README.md renamed to instrumentation-genai/opentelemetry-instrumentation-mcp/examples/README.md

File renamed without changes.

opentelemetry-instrumentation/example/mcp/__init__.py renamed to instrumentation-genai/opentelemetry-instrumentation-mcp/examples/__init__.py

File renamed without changes.

opentelemetry-instrumentation/example/mcp/client.py renamed to instrumentation-genai/opentelemetry-instrumentation-mcp/examples/client.py

File renamed without changes.

opentelemetry-instrumentation/example/mcp/demo.py renamed to instrumentation-genai/opentelemetry-instrumentation-mcp/examples/demo.py

File renamed without changes.

opentelemetry-instrumentation/example/mcp/server.py renamed to instrumentation-genai/opentelemetry-instrumentation-mcp/examples/server.py

File renamed without changes.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "opentelemetry-instrumentation-mcp"
7+
dynamic = ["version"]
8+
description = "OpenTelemetry MCP (Model Context Protocol) instrumentation"
9+
readme = "README.rst"
10+
license = "Apache-2.0"
11+
requires-python = ">=3.8, <=3.13"
12+
authors = [
13+
{ name = "LoongSuite Python Agent Authors"},
14+
]
15+
classifiers = [
16+
"Development Status :: 4 - Beta",
17+
"Intended Audience :: Developers",
18+
"License :: OSI Approved :: Apache Software License",
19+
"Programming Language :: Python",
20+
"Programming Language :: Python :: 3",
21+
"Programming Language :: Python :: 3.8",
22+
"Programming Language :: Python :: 3.9",
23+
"Programming Language :: Python :: 3.10",
24+
"Programming Language :: Python :: 3.11",
25+
"Programming Language :: Python :: 3.12",
26+
"Programming Language :: Python :: 3.13",
27+
]
28+
dependencies = [
29+
"opentelemetry-api <= 1.35.0",
30+
"wrapt",
31+
]
32+
33+
[project.optional-dependencies]
34+
instruments = [
35+
"mcp >= 1.3.0, <= 1.13.1",
36+
]
37+
test = [
38+
"opentelemetry-sdk",
39+
"fastmcp>=2.10.1",
40+
"pytest>=8.4.1",
41+
"pytest-asyncio>=1.0.0",
42+
"websockets",
43+
"pytest-cov",
44+
"Pillow",
45+
"mcp",
46+
"starlette",
47+
"pydantic",
48+
]
49+
type-check = []
50+
51+
[project.entry-points.opentelemetry_instrumentor]
52+
mcp = "opentelemetry.instrumentation.mcp:MCPInstrumentor"
53+
54+
[project.urls]
55+
Homepage = "https://github.com/alibaba/loongsuite-python-agent/tree/main/instrumentation-genai/opentelemetry-instrumentation-mcp"
56+
Repository = "https://github.com/alibaba/loongsuite-python-agent"
57+
58+
[tool.hatch.version]
59+
path = "src/opentelemetry/instrumentation/mcp/version.py"
60+
61+
[tool.hatch.build.targets.sdist]
62+
include = [
63+
"src",
64+
]
65+
66+
[tool.hatch.build.targets.wheel]
67+
packages = ["src/opentelemetry"]
68+
69+
[tool.black]
70+
line-length = 120
71+
72+
[tool.pytest.ini_options]
73+
log_cli = true
74+
xfail_strict = true
75+
addopts = """
76+
--color=yes
77+
"""
78+
filterwarnings = [
79+
"ignore:pkg_resources is deprecated as an API:UserWarning::"
80+
]
81+
[tool.basedpyright]
82+
reportPossiblyUnboundVariable = "none"
83+
[tool.cursorpyright]
84+
reportPossiblyUnboundVariable = "none"
85+
[tool.pyright]
86+
reportPossiblyUnboundVariable = "none"
87+
typeCheckingMode = "off"
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
from typing import Any, Collection
2+
3+
from opentelemetry.instrumentation.mcp.package import _instruments
4+
from opentelemetry import trace as trace_api
5+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
6+
from wrapt import wrap_function_wrapper # type: ignore
7+
from opentelemetry.instrumentation.utils import unwrap
8+
from opentelemetry.metrics import get_meter
9+
from opentelemetry.instrumentation.mcp.session_handler import (
10+
sse_client_wrapper,
11+
stdio_client_wrapper,
12+
websocket_client_wrapper,
13+
streamable_http_client_wrapper,
14+
ServerHandleRequestWrapper,
15+
)
16+
from opentelemetry.instrumentation.mcp.handler import RequestHandler
17+
from opentelemetry.instrumentation.mcp.utils import (
18+
_is_version_supported,
19+
_is_ws_installed,
20+
_get_logger,
21+
)
22+
from opentelemetry.instrumentation.mcp.version import __version__
23+
from opentelemetry.instrumentation.mcp.metrics import ClientMetrics, ServerMetrics
24+
25+
logger = _get_logger(__name__)
26+
27+
_MCP_CLIENT_MODULE = "mcp.client.session"
28+
_MCP_CLIENT_WEBSOCKET_MODULE = "mcp.client.websocket"
29+
_MCP_WEBSOCKET_CLIENT = "websocket_client"
30+
_MCP_CLIENT_SESSION_CLASS = "ClientSession"
31+
32+
33+
RPC_NAME_MAPPING = {
34+
"list_prompts": "prompts/list",
35+
"list_resources": "resources/list",
36+
"list_resource_templates": "resources/templates/list",
37+
"list_tools": "tools/list",
38+
"initialize": "initialize",
39+
"complete": "completion/complete",
40+
"get_prompt": "prompts/get",
41+
"read_resource": "resources/read",
42+
"subscribe_resource": "resources/subscribe",
43+
"unsubscribe_resource": "resources/unsubscribe",
44+
"call_tool": "tools/call",
45+
}
46+
47+
_client_session_methods = [(method_name, rpc_name) for method_name, rpc_name in RPC_NAME_MAPPING.items()]
48+
49+
50+
class MCPInstrumentor(BaseInstrumentor):
51+
"""
52+
An instrumentor for MCP (Model Context Protocol)
53+
"""
54+
55+
def instrumentation_dependencies(self) -> Collection[str]:
56+
return _instruments
57+
58+
def _instrument(self, **kwargs: Any) -> None:
59+
if not _is_version_supported():
60+
logger.warning("MCP version is not supported, skip instrumentation")
61+
return
62+
63+
if not (tracer_provider := kwargs.get("tracer_provider")):
64+
tracer_provider = trace_api.get_tracer_provider()
65+
tracer = trace_api.get_tracer(__name__, __version__, tracer_provider=tracer_provider)
66+
meter = get_meter(
67+
__name__,
68+
__version__,
69+
None,
70+
schema_url="https://opentelemetry.io/schemas/1.11.0",
71+
)
72+
client_metrics = ClientMetrics(meter)
73+
server_metrics = ServerMetrics(meter)
74+
75+
# ClientSession
76+
for method_name, rpc_name in _client_session_methods:
77+
wrap_function_wrapper(
78+
module=_MCP_CLIENT_MODULE,
79+
name=f"{_MCP_CLIENT_SESSION_CLASS}.{method_name}",
80+
wrapper=RequestHandler(rpc_name, tracer, client_metrics),
81+
)
82+
83+
# Client transport wrappers
84+
wrap_function_wrapper(
85+
module="mcp.client.sse",
86+
name="sse_client",
87+
wrapper=sse_client_wrapper(),
88+
)
89+
wrap_function_wrapper(
90+
module="mcp.client.streamable_http",
91+
name="streamablehttp_client",
92+
wrapper=streamable_http_client_wrapper(),
93+
)
94+
wrap_function_wrapper(
95+
module="mcp.client.stdio",
96+
name="stdio_client",
97+
wrapper=stdio_client_wrapper(),
98+
)
99+
if _is_ws_installed():
100+
wrap_function_wrapper(
101+
module=_MCP_CLIENT_WEBSOCKET_MODULE,
102+
name=_MCP_WEBSOCKET_CLIENT,
103+
wrapper=websocket_client_wrapper(),
104+
)
105+
106+
# Server request handler
107+
wrap_function_wrapper(
108+
module="mcp.server.lowlevel.server",
109+
name="Server._handle_request",
110+
wrapper=ServerHandleRequestWrapper(tracer, server_metrics),
111+
)
112+
113+
def _uninstrument(self, **kwargs: Any) -> None:
114+
try:
115+
from mcp import ClientSession
116+
117+
for method_name, _ in _client_session_methods:
118+
unwrap(ClientSession, method_name)
119+
except Exception:
120+
logger.warning("Fail to uninstrument ClientSession", exc_info=True)
121+
122+
try:
123+
import mcp.client.sse
124+
125+
unwrap(mcp.client.sse, "sse_client")
126+
except Exception:
127+
logger.warning("Fail to uninstrument sse_client", exc_info=True)
128+
129+
try:
130+
import mcp.client.streamable_http
131+
132+
unwrap(mcp.client.streamable_http, "streamablehttp_client")
133+
except Exception:
134+
logger.warning("Fail to uninstrument streamablehttp_client", exc_info=True)
135+
136+
try:
137+
import mcp.client.stdio
138+
139+
unwrap(mcp.client.stdio, "stdio_client")
140+
except Exception:
141+
logger.warning("Fail to uninstrument stdio_client", exc_info=True)
142+
143+
if _is_ws_installed():
144+
try:
145+
import mcp.client.websocket
146+
147+
unwrap(mcp.client.websocket, "websocket_client")
148+
except Exception:
149+
logger.warning("Fail to uninstrument websocket_client", exc_info=True)
150+
151+
try:
152+
import mcp.server.lowlevel.server
153+
154+
unwrap(mcp.server.lowlevel.server, "Server._handle_request")
155+
except Exception:
156+
logger.warning("Fail to uninstrument Server._handle_request", exc_info=True)

0 commit comments

Comments
 (0)