Skip to content

Commit 10b4587

Browse files
committed
docs: complete GitHub README and MCP runtime displays
1 parent 84aebce commit 10b4587

5 files changed

Lines changed: 213 additions & 24 deletions

File tree

README.md

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ English | [中文](./README_zh.md)
1010

1111
![FlowLens AgentOps dashboard](./docs/assets/flowlens-agentops-dashboard.png)
1212

13-
[Pages Demo](https://try1004.github.io/FlowLens-AgentOps/) | [Quick Start](#quick-start) | [Architecture](./docs/architecture.md) | [Dual MCP Validation](./docs/mcp-validation-guide.md)
13+
[Pages Demo](https://try1004.github.io/FlowLens-AgentOps/) | [Quick Start](#quick-start) | [Architecture](./docs/architecture.md) | [Dual MCP Validation](./docs/mcp-validation-guide.md) | [Maintainer Handoff](./now.md)
1414

1515
> Built on [DeerFlow 2.0](https://github.com/bytedance/deer-flow) under the MIT License. DeerFlow provides the Agent runtime, tools, subagents, memory, MCP, sandbox, and application foundation. FlowLens adds the diagnostics and AgentOps product layer described below.
1616
@@ -45,6 +45,14 @@ The diagnostics path is read-only: it does not mutate checkpoints, prompts, RunR
4545

4646
## Quick Start
4747

48+
### Choose a mode
49+
50+
| Mode | What it proves | Requirements | URL |
51+
| --- | --- | --- | --- |
52+
| Replay | Dashboard interactions and deterministic synthetic failure scenarios | Node.js or Docker; no model key | `http://localhost:5173` or `http://localhost:4173` |
53+
| Connected Runtime | Real DeerFlow runs emitted into the AgentOps event pipeline | Normal DeerFlow configuration, account, and model provider | `http://localhost:2026/agentops` |
54+
| Connected + dual MCP | Runtime delegation, read-only SQLite Analytics MCP, and FlowLens Inspector MCP | Docker Desktop plus the Connected Runtime requirements | `http://localhost:2027/agentops` |
55+
4856
### No-key Replay with pnpm
4957

5058
```bash
@@ -67,6 +75,14 @@ make flowlens-demo
6775

6876
Open `http://localhost:4173`, then stop it with `make flowlens-stop`.
6977

78+
On Windows without `make`, run the Compose command directly:
79+
80+
```powershell
81+
docker compose -f docker/docker-compose-flowlens-demo.yaml up --build
82+
```
83+
84+
Press `Ctrl+C` to stop the foreground Replay container.
85+
7086
### Connected DeerFlow runtime
7187

7288
```bash
@@ -78,11 +94,36 @@ Open `http://localhost:2026/agentops`. Connected mode requires the normal DeerFl
7894

7995
### Connected runtime with dual MCP services
8096

81-
The development compose stack can run two internal Streamable HTTP MCP services.
82-
They use a discovery-only service token for schema loading and a short user/run-bound
83-
delegated token for each tool call. The full isolated Docker command and validation
84-
prompts are in the [Dual MCP Validation Guide](./docs/mcp-validation-guide.md)
85-
and the [Chinese end-to-end test runbook](./docs/flowlens-dual-mcp-test-runbook.zh-CN.md).
97+
The development stack runs two internal-only Streamable HTTP MCP services:
98+
99+
- **SQLite Analytics MCP** exposes schema inspection and `SELECT`-only local demonstration-data queries.
100+
- **FlowLens Inspector MCP** queries owner-checked diagnostic data through the Gateway.
101+
102+
Schemas are discovered with a restricted service token. A real tool invocation receives a short-lived token bound to the current `user_id` and `run_id`; the model never receives that token.
103+
104+
From the repository root in PowerShell:
105+
106+
```powershell
107+
$env:DEER_FLOW_ROOT = (Get-Location).Path
108+
$env:HOME = $env:USERPROFILE
109+
$env:DEER_FLOW_CONTAINER_PREFIX = "flowlens-agentops"
110+
$env:DEER_FLOW_HTTP_PORT = "2027"
111+
112+
docker compose -p flowlens-agentops `
113+
-f docker/docker-compose-dev.yaml `
114+
-f docker/docker-compose-agentops-isolated.yaml `
115+
up -d --build --remove-orphans frontend gateway nginx sqlite-analytics flowlens-inspector
116+
117+
Invoke-RestMethod http://localhost:2027/health
118+
```
119+
120+
Open `http://localhost:2027/`, sign in, then open `http://localhost:2027/agentops`. To exercise the real SQLite path, start a new chat and send:
121+
122+
```text
123+
Use the SQLite Analytics MCP to inspect the available schema, describe the agent_runs table, then count runs by status. Execute read-only SQL only, state the SQL used, and label the result as local demonstration data.
124+
```
125+
126+
The selected Run should show MCP events in Timeline and an `MCP > 0` metric. The [Dual MCP Validation Guide](./docs/mcp-validation-guide.md) and [Chinese end-to-end runbook](./docs/flowlens-dual-mcp-test-runbook.zh-CN.md) cover expected evidence, policy-rejection cases, and shutdown.
86127

87128
## Diagnostic APIs
88129

@@ -134,8 +175,10 @@ FlowLens does not claim authorship of the DeerFlow runtime modules. See [NOTICE]
134175

135176
| Gate | Result |
136177
| ---------------------------------- | ------------------------------------------------------------------ |
137-
| Focused backend FlowLens/API suite | 53 tests passed |
138-
| Frontend Vitest suite | 219 tests passed |
178+
| Focused backend FlowLens/API suite | 58 tests passed on 2026-07-31 |
179+
| Frontend Vitest suite | 232 tests passed on 2026-07-31 |
180+
| Frontend quality gate | ESLint, TypeScript, Next production build, and Vite Replay build passed on 2026-07-31 |
181+
| Real MCP integration probe | `ToolNode -> delegated OAuth token -> SQLite schema tool` completed locally |
139182
| Next and static Replay Playwright | 5 tests passed across route, desktop, tablet, and mobile workflows |
140183
| Secret scan | No high-confidence credentials in Git-tracked files |
141184
| In-process diagnostics benchmark | 2,000 events, 20 iterations, median 24.7578 ms, P95 36.5606 ms |
@@ -165,6 +208,8 @@ The machine-readable result is committed at [flowlens-benchmark.json](./docs/ass
165208
- [Failure attribution rules](./docs/failure-attribution.md)
166209
- [Replay and Connected demo guide](./docs/demo-guide.md)
167210
- [Dual MCP validation guide](./docs/mcp-validation-guide.md)
211+
- [Chinese dual MCP end-to-end runbook](./docs/flowlens-dual-mcp-test-runbook.zh-CN.md)
212+
- [Maintainer handoff and next steps](./now.md)
168213
- [Contributing](./CONTRIBUTING.md)
169214
- [Changelog](./CHANGELOG.md)
170215

frontend/src/components/agentops/event-detail-sheet.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ function metadataValue(value: unknown): string {
99
return JSON.stringify(value);
1010
}
1111

12+
function metadataString(
13+
metadata: TimelineItem["metadata"],
14+
keys: string[],
15+
): string | null {
16+
for (const key of keys) {
17+
const value = metadata[key];
18+
if (typeof value === "string" && value.trim()) return value;
19+
}
20+
return null;
21+
}
22+
1223
export function EventDetailSheet({
1324
item,
1425
onClose,
@@ -23,6 +34,12 @@ export function EventDetailSheet({
2334
}, [item?.seq]);
2435

2536
if (!item) return null;
37+
const isMcp = item.phase === "mcp";
38+
const mcpServer = isMcp
39+
? (metadataString(item.metadata, ["server_name", "server", "mcp_server"]) ??
40+
"Not reported")
41+
: null;
42+
2643
return (
2744
<div
2845
className="fixed inset-0 z-50 flex justify-end bg-black/25"
@@ -55,7 +72,9 @@ export function EventDetailSheet({
5572
<dl className="grid grid-cols-[7rem_1fr] gap-x-3 gap-y-3">
5673
<dt className="agentops-muted">Phase</dt>
5774
<dd className="capitalize">{item.phase}</dd>
58-
<dt className="agentops-muted">Component</dt>
75+
<dt className="agentops-muted">
76+
{isMcp ? "MCP tool" : "Component"}
77+
</dt>
5978
<dd className="break-words">{item.component}</dd>
6079
<dt className="agentops-muted">Status</dt>
6180
<dd className="capitalize">{item.status}</dd>
@@ -73,6 +92,20 @@ export function EventDetailSheet({
7392
</dd>
7493
</dl>
7594

95+
{isMcp ? (
96+
<div className="agentops-divider border-y py-4">
97+
<h3 className="agentops-eyebrow">MCP context</h3>
98+
<dl className="mt-3 grid grid-cols-[7rem_1fr] gap-x-3 gap-y-3">
99+
<dt className="agentops-muted">Server</dt>
100+
<dd className="font-mono text-xs break-all">{mcpServer}</dd>
101+
<dt className="agentops-muted">Tool lifecycle</dt>
102+
<dd className="font-mono text-xs break-all">
103+
{item.event_type}
104+
</dd>
105+
</dl>
106+
</div>
107+
) : null}
108+
76109
<div>
77110
<h3 className="agentops-eyebrow">Summary</h3>
78111
<p className="mt-2 leading-6">{item.summary}</p>

frontend/src/components/agentops/metrics-strip.tsx

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import type { RunMetrics, TimelineItem } from "@/core/agentops/types";
22

3+
type MetricCell = {
4+
label: string;
5+
value: string;
6+
detail?: string;
7+
detailClassName?: string;
8+
};
9+
310
function durationLabel(durationMs: number | null): string {
411
if (durationMs === null) return "--";
512
if (durationMs < 1000) return `${durationMs} ms`;
@@ -14,28 +21,37 @@ export function MetricsStrip({
1421
timeline: TimelineItem[];
1522
}) {
1623
const mcpEvents = timeline.filter((item) => item.phase === "mcp").length;
24+
const mcpIncidents = timeline.filter(
25+
(item) => item.phase === "mcp" && item.status === "error",
26+
).length;
1727
const memoryEvents = timeline.filter(
1828
(item) => item.phase === "memory",
1929
).length;
20-
const cells = [
21-
["Duration", durationLabel(metrics.duration_ms)],
22-
["Tokens", metrics.total_tokens.toLocaleString()],
23-
[
24-
"Tools / Errors",
25-
`${metrics.tool_call_count} / ${metrics.tool_error_count}`,
26-
],
27-
[
28-
"Subagents",
29-
`${metrics.subagent_count} / ${metrics.subagent_error_count}`,
30-
],
31-
["MCP", String(mcpEvents)],
32-
["Memory", String(memoryEvents)],
33-
["Status", metrics.final_status],
30+
const cells: MetricCell[] = [
31+
{ label: "Duration", value: durationLabel(metrics.duration_ms) },
32+
{ label: "Tokens", value: metrics.total_tokens.toLocaleString() },
33+
{
34+
label: "Tools / Errors",
35+
value: `${metrics.tool_call_count} / ${metrics.tool_error_count}`,
36+
},
37+
{
38+
label: "Subagents",
39+
value: `${metrics.subagent_count} / ${metrics.subagent_error_count}`,
40+
},
41+
{
42+
label: "MCP",
43+
value: mcpEvents === 1 ? "1 event" : `${mcpEvents} events`,
44+
detail: mcpIncidents > 0 ? `${mcpIncidents} incident` : undefined,
45+
detailClassName:
46+
mcpIncidents > 0 ? "agentops-metric-danger" : undefined,
47+
},
48+
{ label: "Memory", value: String(memoryEvents) },
49+
{ label: "Status", value: metrics.final_status },
3450
];
3551

3652
return (
3753
<dl className="agentops-metrics grid min-h-20 grid-cols-2 border-b sm:grid-cols-4 xl:grid-cols-7">
38-
{cells.map(([label, value]) => (
54+
{cells.map(({ label, value, detail, detailClassName }) => (
3955
<div
4056
key={label}
4157
className="agentops-metric-cell flex min-w-28 flex-col justify-center border-r border-b px-5 py-3.5 last:border-r-0 sm:border-b-0"
@@ -46,6 +62,13 @@ export function MetricsStrip({
4662
<dd className="mt-1 truncate text-[15px] font-semibold capitalize tabular-nums">
4763
{value}
4864
</dd>
65+
{detail ? (
66+
<p
67+
className={`mt-0.5 truncate text-[10px] font-medium ${detailClassName ?? "agentops-muted"}`}
68+
>
69+
{detail}
70+
</p>
71+
) : null}
4972
</div>
5073
))}
5174
</dl>

frontend/tests/unit/components/event-detail-sheet.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,28 @@ describe("EventDetailSheet", () => {
3434
expect(disclosure).toHaveAttribute("aria-expanded", "true");
3535
expect(screen.getByText("tool_name")).toBeVisible();
3636
});
37+
38+
test("summarizes MCP service and tool context before raw metadata", () => {
39+
render(
40+
<EventDetailSheet
41+
item={{
42+
...item,
43+
phase: "mcp",
44+
event_type: "mcp.tool.end",
45+
component: "sqlite_analytics_query_readonly",
46+
metadata: {
47+
server_name: "sqlite-analytics",
48+
tool_name: "sqlite_analytics_query_readonly",
49+
},
50+
}}
51+
onClose={vi.fn()}
52+
/>,
53+
);
54+
55+
expect(screen.getByText("MCP context")).toBeVisible();
56+
expect(screen.getByText("MCP tool")).toBeVisible();
57+
expect(screen.getByText("sqlite-analytics")).toBeVisible();
58+
expect(screen.getByText("Tool lifecycle")).toBeVisible();
59+
expect(screen.getAllByText("mcp.tool.end")).toHaveLength(2);
60+
});
3761
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { render, screen } from "@testing-library/react";
2+
import { describe, expect, test } from "vitest";
3+
4+
import { MetricsStrip } from "@/components/agentops/metrics-strip";
5+
import type { RunMetrics, TimelineItem } from "@/core/agentops/types";
6+
7+
const metrics: RunMetrics = {
8+
duration_ms: 2400,
9+
total_events: 4,
10+
total_tokens: 1200,
11+
tool_calls: 0,
12+
errors: 1,
13+
tool_call_count: 0,
14+
tool_error_count: 0,
15+
subagent_count: 0,
16+
subagent_error_count: 0,
17+
rollback_performed: false,
18+
final_status: "success",
19+
failure_category: "MCP_TOOL_ERROR",
20+
data_completeness: "complete",
21+
first_event_latency_ms: 15,
22+
};
23+
24+
const mcpTimeline: TimelineItem[] = [
25+
{
26+
seq: 1,
27+
timestamp: "2026-07-31T10:00:00.000Z",
28+
phase: "mcp",
29+
event_type: "mcp.tool.start",
30+
component: "sqlite_analytics_schema_overview",
31+
status: "running",
32+
summary: "SQLite Analytics tool started",
33+
duration_ms: null,
34+
span_id: "mcp-span",
35+
parent_span_id: "agent-span",
36+
malformed: false,
37+
metadata: {},
38+
},
39+
{
40+
seq: 2,
41+
timestamp: "2026-07-31T10:00:01.000Z",
42+
phase: "mcp",
43+
event_type: "mcp.tool.error",
44+
component: "sqlite_analytics_schema_overview",
45+
status: "error",
46+
summary: "SQLite Analytics tool rejected the query",
47+
duration_ms: 12,
48+
span_id: "mcp-span",
49+
parent_span_id: "agent-span",
50+
malformed: false,
51+
metadata: {},
52+
},
53+
];
54+
55+
describe("MetricsStrip", () => {
56+
test("surfaces MCP activity and incidents separately", () => {
57+
render(<MetricsStrip metrics={metrics} timeline={mcpTimeline} />);
58+
59+
expect(screen.getByText("2 events")).toBeVisible();
60+
expect(screen.getByText("1 incident")).toHaveClass(
61+
"agentops-metric-danger",
62+
);
63+
});
64+
});

0 commit comments

Comments
 (0)