feat: expose monoscope as MCP server at /api/v1/mcp#367
Conversation
Adds a JSON-RPC endpoint that surfaces every ApiV1Routes operation as a typed MCP tool. The registry is derived once from apiV1OpenApiSpec at startup, so new endpoints become tools without touching MCP code; tool calls are forwarded as synthetic WAI requests into a re-served inner ApiV1Routes Application (no second auth pass, no handler duplication). Tools cover initialize, tools/list, tools/call, and notifications/*. Tool names are derived from path+method (e.g. monoscope_schema_get, monoscope_monitors_monitor_id_get) since servant-openapi3 does not emit operationIds for our spec; if operationIds are added later the registry prefers them. Tests cover registry shape, tools/list round-trip, initialize, tools/call round-tripping monoscope_schema_get into Schema.telemetrySchema, and the unknown-tool isError-not-JSON-RPC-error semantics.
Replaces auto-derived monoscope_<resource>_<id>_<action>_<method> names with the verb-first snake_case convention every peer MCP server uses (Sentry, Grafana, Datadog, Honeycomb): list_monitors, get_monitor, mute_monitor, search_events, apply_dashboard, whoami, etc. Names come from a canonical (method, path) override map in MCP.hs; unmapped routes fall back to the path-derived name. The MCP endpoint itself is excluded so agents cannot recurse into it. Drops the redundant "monoscope_" prefix — MCP clients already namespace tools by server name.
Adds three Sentry/Grafana-style workflow tools alongside the OpenAPI- derived REST tools: - find_error_patterns — top established log patterns ranked by current hour count. Wraps getPatternsWithCurrentRates. - search_events_nl — natural-language → KQL via runAgenticQuery; agent then calls search_events with the result. - analyze_issue — fetches the issue and asks the LLM for a structured diagnosis (probable cause / signals / next steps). Refactor: collapses ToolEntry + CompositeTool into one Tool record with a Dispatch sum (ViaOpenApi | ViaComposite) — drops field-name prefixes, single dispatcher path, single registry. allTools merges REST + composite once at startup. Derives ToJSON for LogPatternWithRate (snake_case via deriving-aeson) so the composite tool returns it directly without a manual encoder. Tests now cover the full dispatch chain: in-process registry/list/call checks plus three e2e tests that POST through the real /api/v1/mcp HTTP route with an API key and assert auth (401 without, 200 with) and tool results (get_schema round-trips through to telemetrySchema).
Code Review — feat: MCP server at
|
| Issue | File | Priority |
|---|---|---|
Reimplemented clamp |
Web/MCP.hs:647 |
Low |
deref vs preview OA._Inline |
Web/MCP.hs:318 |
Low |
| Nested list comprehension | Web/MCP.hs:330 |
Low |
bodyContentSchema case → listToMaybe |
Web/MCP.hs:347 |
Low |
| Test verbosity | ApiV1Spec.hs:855 |
Low |
Core logic, auth threading, the NOINLINE CAF registry, and URL encoding in splitArgs all look correct. The test coverage (unit + e2e auth rejection + e2e round-trip) is solid.
- use Data.Ord.clamp (tuple-form) instead of custom helper - use preview OA._Inline directly; drop deref helper - simplify requireds: ["body" | bodyReq && isJust bodySchema] - bodyContentSchema via listToMaybe instead of pattern match - prefer Relude encodeUtf8/decodeUtf8 (drops Data.Text.Encoding + Data.ByteString.Lazy qualified imports) - use Relude unlines/Down (drop Data.Ord.Down + T.unlines) - collapse tools/list test into a single ^.. + for_ pass - comment notifications/* response semantics for the HTTP transport
|
test |
Open-source observability platform with S3 storageIngest and explore logs, traces, and metrics stored in your S3 buckets. Query with natural language. Create AI agents that detect anomalies and send daily/weekly reports to your inbox. Website • Playground • Discord • Twitter • Documentation What is Monoscope?Monoscope is an open-source observability platform that stores your telemetry data in S3-compatible storage. Self-host it or use our cloud offering. Core capabilities:
Cloud vs Self-hostedIn both options, you bring your own S3 buckets—your data stays yours.
→ Start free on Cloud or continue below to self-host. Quick Startgit clone https://github.com/monoscope-tech/monoscope.git
cd monoscope
docker-compose upVisit Send Test DataPopulate your dashboard with test telemetry: # Install telemetrygen
go install github.com/open-telemetry/opentelemetry-collector-contrib/cmd/telemetrygen@latest
# Send test traces (replace YOUR_API_KEY from the UI)
telemetrygen traces --otlp-endpoint localhost:4317 --otlp-insecure \
--otlp-header 'Authorization="Bearer YOUR_API_KEY"' --traces 10CLIManage your Monoscope project from the terminal — search logs, query metrics, manage monitors and dashboards, triage issues, and more. curl -fsSL https://monoscope.tech/install.sh | bash
monoscope auth loginSee the CLI reference for the full command list. Agentic pipelineEvery CLI command emits a stable JSON envelope, so you can chain discovery → # 1. What services exist? (Precomputed; no aggregation query needed.)
SVC=$(monoscope facets resource.service.name --top 1 \
| jq -r '.["resource.service.name"][0].value')
# 2. Grab one error event from that service — id only, ready to chain.
ID=$(monoscope logs search 'severity.text=="error"' \
--service "$SVC" --first --id-only)
# 3. Pull the surrounding 5 minutes of traffic, with a per-trace summary
# showing which other services were affected.
monoscope events context --window 5m --summary \
--at "$(monoscope events get "$ID" | jq -r .timestamp)" \
| jq '.traces | sort_by(-.error_count) | .[0:3]'
# 4. Acknowledge the open issue once you have a hypothesis.
monoscope issues list --service "$SVC" --status open \
| jq -r '.data[].id' | head -3 \
| xargs -I {} monoscope issues ack {}Each step's output shape is documented and stable:
Set Claude Code SkillsLet Claude investigate incidents, triage alerts, and write KQL queries using the # Claude Code
claude plugin marketplace add monoscope-tech/skills
claude plugin install monoscope-skills@monoscope-skills
# or via npx (Cursor, Cline, Copilot, and other agents)
npx skills add monoscope-tech/skillsRestart Claude Code after installation. Skills activate automatically when relevant — e.g. "investigate the 500 errors in payment-api" or "do an on-call sweep". See github.com/monoscope-tech/skills for the full skill list and documentation. IntegrationAuto-instrument your appsPythonpip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
OTEL_SERVICE_NAME="my-app" \
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317" \
opentelemetry-instrument python myapp.pyNode.jsnpm install --save @opentelemetry/auto-instrumentations-node
OTEL_SERVICE_NAME="my-app" \
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317" \
node --require @opentelemetry/auto-instrumentations-node/register app.jsJavacurl -L https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar -o otel-agent.jar
OTEL_SERVICE_NAME="my-app" \
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317" \
java -javaagent:otel-agent.jar -jar myapp.jarKubernetes# Install OpenTelemetry Operator
kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml
# Configure auto-instrumentation
kubectl apply -f - <<EOF
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
name: my-instrumentation
spec:
exporter:
endpoint: http://monoscope:4317
propagators:
- tracecontext
- baggage
EOF
# Annotate your deployments for auto-instrumentation
kubectl patch deployment my-app -p \
'{"spec":{"template":{"metadata":{"annotations":{"instrumentation.opentelemetry.io/inject-java":"my-instrumentation"}}}}}'Natural Language QueriesQuery your telemetry data in plain English:
AI Agents & ReportsCreate AI agents that monitor your systems on a schedule:
Architecturegraph LR
A[Your Apps] -->|Logs/Metrics/Traces| B[Ingestion API]
B --> C[TimeFusion Engine]
C --> D[(S3 Storage)]
D --> E[Query Engine]
E --> F[Dashboards]
D --> G[AI Agent Scheduler]
G -->|LLM Analysis| H[Anomaly Detection]
H --> I[Email Reports]
H --> J[Alert Channels]
Powered by TimeFusionMonoscope is built on TimeFusion, our open-source time-series database for observability workloads.
How It Compares
ScreenshotsLog Explorer - Unified ViewLogs and trace spans displayed together in context for complete observability.
Trace Context IntegrationSee detailed trace information alongside logs for debugging complex distributed systems.
Dashboard AnalyticsReal-time metrics and performance monitoring with AI-powered insights.
Trusted by Leading Companies
DocumentationRoadmap
See our public roadmap for details and to vote on features. Community💬 Discord • 🐛 Issues • 🐦 Twitter LicenseAGPL-3.0. See LICENSE for details. For commercial licensing options, contact us at [email protected]. |
Code Review |
The previous "apply review feedback" commit was supposed to land all of these in MCP.hs but only the test-file edits made it through after the rebase conflict resolution picked the wrong side. This puts them back: - Data.Ord.clamp (tuple-form); drop custom helper - preview OA._Inline; drop deref helper - requireds collapses to ["body" | bodyReq && isJust bodySchema] - bodyContentSchema via listToMaybe - Relude encodeUtf8/decodeUtf8 throughout (drops Data.Text.Encoding + Data.ByteString.Lazy qualified imports) - unlines/Down from Relude (drop Data.Ord.Down + T.unlines) - comment notifications/* HTTP-vs-spec semantics
Code Review — PR #367: MCP server at
|
|
DELETE_ME_4365761583 |
|
Code Review The architecture is clean: registry built once from apiV1OpenApiSpec, OpenAPI tools forwarded as synthetic WAI requests, composite workflow tools layered on top. A few things to tighten. Succinctness deriveNameFromPath - two T.replace calls can be one T.filter. The codebase already uses T.filter elsewhere (e.g. GitSync.hs). Replace: T.replace "{" "" dollar T.replace "}" "" p with T.filter (notElem "{}") p. mkInputSchema - maybe id pattern is more idiomatic and shorter: maybe id (KM.insert "body") bodySchema paramProps instead of maybe paramProps (lambda b -> KM.insert "body" b paramProps) bodySchema. Potential bugs callOpenApi has no timeout on the inner WAI session. WT.runSession is synchronous - if the forwarded handler hangs (DB call, AI call), the MCP endpoint blocks indefinitely. The outer HTTP request timeout already fired before we entered the handler, so it will not save us. Wrap with System.Timeout.timeout. bodyContentSchema picks the first media type arbitrarily via listToMaybe (IOH.elems ...). Not a current bug (all v1 routes are JSON-only) but worth a note so a future multi-content-type endpoint does not silently get the wrong schema. allTools merge order: the comment says composite tools shadow OpenAPI tools but does not say why. Map (<>) is left-biased union, so composite-first is correct but subtle. A one-liner prevents a future reader from accidentally flipping the sides. Security analyzeIssue has a prompt injection surface. renderJson (AE.toJSON issue) is spliced directly into the LLM prompt. Issue message or stack-trace fields containing adversarial content can hijack the analysis. Wrapping the payload section with clear delimiters (triple-backtick fences, XML tags, etc.) reduces the attack surface. searchEventsNL passes the timezone argument through unvalidated. It flows directly into AI.defaultAgenticConfig. An invalid value will not crash but will be silently misinterpreted by the LLM. Minor protocolVersion "2025-06-18" should be a named constant so it is easy to update at the next spec bump. runTool only catches Servant.ServerError from ViaComposite handlers. DB and other effectful exceptions propagate out as HTTP 500s instead of isError: true tool results. The Dispatch constructor comment promises ServerErrors are caught - consider catching SomeException to honour that contract fully. Test let mkTopApp and let mcpHttp are defined mid-describe after several it blocks. Moving them to the top let group alongside reg and dummyApp would be more consistent. LogPatternWithRate JSON derivation matches the LogPattern pattern above it. Correct. Overall: the WAI test session approach for inner dispatch is clever and avoids all handler duplication. Test coverage of key semantics (unknown tool returns isError not JSON-RPC error, auth rejection, round-trip through real HTTP route) is solid. |
Succinctness: - deriveNameFromPath: one T.filter instead of two T.replace - mkInputSchema: maybe id (KM.insert "body") bodySchema paramProps Robustness: - runTool: 30s System.Timeout cap so a hung sub-handler can't block the MCP endpoint; outer HTTP timeout fires too late to save us. - runTool composite branch now also catches SomeException (was only ServerError) so DB / AI exceptions surface as isError tool results instead of HTTP 500s — matches the Dispatch comment's promise. Security: - analyzeIssue prompt fences the issue payload in <issue> tags and tells the model to treat its contents as data, reducing the prompt-injection surface from adversarial log/exception text. - searchEventsNL now sanitizes the timezone arg (trim, length cap, alphanum + /_-+) before threading it into AgenticConfig. Polish: - protocolVersion lifted to mcpProtocolVersion constant. - bodyContentSchema commented re first-media-type assumption. - allTools merge order commented (Map (<>) is left-biased; composites win on collision). - Test mkTopApp + mcpHttp helpers moved up alongside reg/dummyApp.
Code Review: feat: expose monoscope as MCP server at /api/v1/mcpOverall a well-structured PR. The architecture (OpenAPI → tool registry, sub-app forwarding, composite tools) is clean and the test coverage is solid. A few issues worth addressing before merging: Critical / Correctness
Inner sub-app re-exposes the , (m, p) /= ("POST", "/mcp") -- never expose MCP itself as a toolbut the sub-app itself still has that route live. If a composite tool ever forwards to
mkApiV1App p =
genericServeTWithContext
(effToServantHandler env logger tp)
(apiV1Server logger env tp p)
Servant.EmptyContext
Minor IssuesNo response size cap in
intArg k o = KM.lookup (AK.fromText k) o >>= \case AE.Number n -> Just (round n); _ -> NothingIf an agent passes
Conciseness / Style
-- current
Map.fromList [(t.name, t) | t <- compositeTools] <> mkToolsFromOpenApi spec
-- slightly tighter with OverloadedRecordDot already in scope
Map.fromList (map (\t -> (t.name, t)) compositeTools) <> mkToolsFromOpenApi spec
-- or
foldMap' (\t -> Map.singleton t.name t) compositeTools <> mkToolsFromOpenApi spec
"properties" AE..= AE.object [(AK.fromText k, v) | (k, v) <- props]Can use
Unnecessary explicit "isError" AE..= False -- Bool is inferred, no need for AE.Bool False cast in toolError eitherMinor, but consistent with how Test CoverageGood coverage overall. Missing:
Nits
|
Correctness: - callOpenApi rejects path == "/mcp" defensively. The registry already filters it, so this only fires if someone adds an OpenApiBinding pointing at the MCP route — fail fast instead of recursing. - Build the inner Servant Application once per tools/call (in handleJsonRpc) instead of on every ViaOpenApi dispatch inside runTool. runTool now takes the prebuilt Application directly. Robustness: - Cap REST tool response bodies at 64 KB before stuffing them into the MCP `content` field; structuredContent retains the full untruncated JSON for typed clients. - intArg uses `floor` (was `round`) — `limit: 4.9` should not silently become 5. Schema accuracy: - bodyContentSchema prefers application/json before falling back to whatever's first; matches how MCP forwards request bodies. Documentation: - Comment justifying Network.Wai.Test usage from a library module (wai-extra is already an explicit lib dep, this is the cheapest way to drive an Application synchronously). Tests: - notifications/* returns AE.Null with no envelope. - tools/call create_team exercises POST body forwarding + round-trip.
Code Review — MCP Server (#367)Well-structured addition overall. The registry-at-startup + WAI forwarding architecture is clean, the composite tools pattern is useful, and test coverage is solid. Some issues worth addressing: Bug / Correctness
let bodyBs = LBS.toStrict (LBS.take (fromIntegral maxToolBodyBytes + 1) bodyLBS)
bodyTxt = decodeUtf8 bodyBs <> if LBS.length bodyLBS > fromIntegral maxToolBodyBytes then "\n…[truncated, full payload in structuredContent]" else ""
intArg k o = KM.lookup (AK.fromText k) o >>= \case AE.Number n -> Just (floor n); _ -> Nothing
Performance
Conciseness / Packages
-- current
T.filter (`notElem` ("{}" :: String)) p
-- clearer and avoids the String coercion
T.filter (\c -> c /= '{' && c /= '}') p
pathItemOps p = catMaybes
[ (m,) <$> p ^. l
| (m, l) <-
[ ("GET", OA.get), ("POST", OA.post), ("PUT", OA.put), ("PATCH", OA.patch)
, ("DELETE", OA.delete), ("HEAD", OA.head_), ("OPTIONS", OA.options), ("TRACE", OA.trace)
]
]
toolsListJson reg =
AE.object
[ "tools" AE..=
[ AE.object ["name" AE..= t.name, "description" AE..= t.description, "inputSchema" AE..= t.inputSchema]
| t <- Map.elems reg
]
]Security
Minor
|



Summary
/api/v1/mcpJSON-RPC endpoint exposes everyApiV1Routesoperation as an MCP tool — agents (Claude Code, Cursor, etc.) get typed tool schemas instead of one generic call.apiV1OpenApiSpecat startup; new endpoints become tools automatically with no MCP-side maintenance.ApiV1RoutesApplication— no second auth pass, no handler duplication.How it's wired
src/Web/MCP.hs(~250 LOC) —ToolEntry,mkToolsFromOpenApi,handleJsonRpc,runOperation, JSON-Schema helpers.src/Web/Routes.hs— addedmcpfield toApiV1Routes(re-uses the existingAuthProtect "api-key-auth"); widenedserver/apiV1Serverto threadLogger/AuthContext/TracerProviderso the MCP handler can rebuild the inner sub-app.src/System/Server.hs—mkServerpasses through the threaded args.package.yaml— addedinsert-ordered-containers(already transitive viaopenapi3).Supports
initialize,tools/list,tools/call, andnotifications/*. Tool names are derived frompath+method(e.g.monoscope_schema_get,monoscope_monitors_monitor_id_get); the code prefers an explicitoperationIdif/when servant-openapi3 starts emitting them.Test plan
test/integration/Web/ApiV1Spec.hsunderdescribe "MCP"— registry shape,tools/listround-trip,initialize,tools/call monoscope_schema_getround-tripping intoSchema.telemetrySchema, and unknown-tool isError-not-JSON-RPC-error semantics. All 5 pass locally.~/.claude.jsonand confirm tools appear under/mcp.