feat: refactor node spec and add mcp tools (#244)

* refactor: carve out extraction panel

* refactor: create spec versions for node types

* refactor: create a GenericNode and remove custom nodes

* feat: add python and typescript sdk

* add dograh sdk

* fix: fetch draft workflow definition over published one

* fix: fix routes of SDKs to use code gen

* chore: remove doclink dependency to reduce image size

* chore: format files

* chore: bump pipecat

* feat: let mcp fetch archived workflows on demand

* chore: fix tests

* feat: add sdk documentation

* chore: change banner and add badge
This commit is contained in:
Abhishek 2026-04-21 07:56:16 +05:30 committed by GitHub
parent 0a61ef295f
commit 00a1a22b74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
162 changed files with 14355 additions and 3554 deletions

87
api/mcp_server/tracing.py Normal file
View file

@ -0,0 +1,87 @@
"""OTel tracing for MCP tool invocations.
The project-wide tracing setup in
`api/services/pipecat/tracing_config.py` already routes spans to
per-organization Langfuse projects based on the `dograh.org_id` span
attribute. This module plugs MCP tool calls into that pipeline:
@mcp.tool
@traced_tool
async def my_tool(...): ...
Each decorated invocation produces one span named `mcp.<tool_name>` with
Langfuse-rendered input/output. Organization and user attributes are
stamped separately by `authenticate_mcp_request` when it runs inside
the tool body the decorator's span is the `current_span` at that
point, so the attributes land on the right span and the router export
dispatches to the correct Langfuse project.
"""
from __future__ import annotations
import json
from functools import wraps
from typing import Any, Awaitable, Callable, TypeVar
from opentelemetry import trace
from opentelemetry.context import Context
from opentelemetry.trace import Status, StatusCode
R = TypeVar("R")
_TRACER = trace.get_tracer("dograh.mcp")
# Langfuse truncates long payloads anyway; cap here to keep span size
# bounded. Tune up if you find tool outputs consistently clipped.
_MAX_ATTR_LEN = 8000
def _safe_json(value: Any) -> str:
try:
return json.dumps(value, default=str, ensure_ascii=False)
except Exception: # noqa: BLE001
return str(value)
def traced_tool(fn: Callable[..., Awaitable[R]]) -> Callable[..., Awaitable[R]]:
"""Wrap an MCP tool so each invocation produces a span.
Captures tool name, input kwargs, output, and exceptions. Stacks
below `@mcp.tool` so FastMCP sees the wrapped function when
introspecting the tool schema (`functools.wraps` preserves the
signature the framework reads).
"""
@wraps(fn)
async def wrapper(*args: Any, **kwargs: Any) -> R:
# Each MCP tool call is its own root trace. Passing an empty
# `Context()` severs the inherited parent so the span doesn't
# graft onto whatever other trace happens to be active (e.g.
# the FastAPI request span, or a client-propagated context).
# One trace per tool invocation makes Langfuse diffing and
# per-org filtering clean.
with _TRACER.start_as_current_span(
f"mcp.{fn.__name__}",
context=Context(),
) as span:
span.set_attribute("mcp.tool.name", fn.__name__)
# Explicit trace-name override so the Langfuse UI shows
# `mcp.<tool>` at the top of the trace instead of whatever
# the framework happens to name the root span.
span.set_attribute("langfuse.trace.name", f"mcp.{fn.__name__}")
span.set_attribute(
"langfuse.observation.input",
_safe_json(kwargs)[:_MAX_ATTR_LEN],
)
try:
result = await fn(*args, **kwargs)
except Exception as e:
span.record_exception(e)
span.set_status(Status(StatusCode.ERROR, str(e)))
raise
span.set_attribute(
"langfuse.observation.output",
_safe_json(result)[:_MAX_ATTR_LEN],
)
return result
return wrapper