mcp-samply 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +14 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/profile/analyze.d.ts +162 -0
  7. package/dist/profile/analyze.js +745 -0
  8. package/dist/profile/analyze.js.map +1 -0
  9. package/dist/profile/store.d.ts +104 -0
  10. package/dist/profile/store.js +238 -0
  11. package/dist/profile/store.js.map +1 -0
  12. package/dist/samply/executable.d.ts +3 -0
  13. package/dist/samply/executable.js +34 -0
  14. package/dist/samply/executable.js.map +1 -0
  15. package/dist/samply/process.d.ts +11 -0
  16. package/dist/samply/process.js +39 -0
  17. package/dist/samply/process.js.map +1 -0
  18. package/dist/samply/record.d.ts +36 -0
  19. package/dist/samply/record.js +180 -0
  20. package/dist/samply/record.js.map +1 -0
  21. package/dist/server.d.ts +2 -0
  22. package/dist/server.js +22 -0
  23. package/dist/server.js.map +1 -0
  24. package/dist/source/locate.d.ts +31 -0
  25. package/dist/source/locate.js +284 -0
  26. package/dist/source/locate.js.map +1 -0
  27. package/dist/tools/doctor.d.ts +2 -0
  28. package/dist/tools/doctor.js +81 -0
  29. package/dist/tools/doctor.js.map +1 -0
  30. package/dist/tools/locate.d.ts +2 -0
  31. package/dist/tools/locate.js +85 -0
  32. package/dist/tools/locate.js.map +1 -0
  33. package/dist/tools/profile.d.ts +3 -0
  34. package/dist/tools/profile.js +433 -0
  35. package/dist/tools/profile.js.map +1 -0
  36. package/dist/tools/record.d.ts +3 -0
  37. package/dist/tools/record.js +117 -0
  38. package/dist/tools/record.js.map +1 -0
  39. package/dist/tools/state.d.ts +6 -0
  40. package/dist/tools/state.js +11 -0
  41. package/dist/tools/state.js.map +1 -0
  42. package/package.json +51 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cong-Cong Pan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # mcp-samply
2
+
3
+ `mcp-samply` is a MCP server for [`samply`](https://github.com/mstange/samply).
4
+
5
+ It is designed for AI agents that need to:
6
+
7
+ - record CPU profiles with `samply`
8
+ - load existing `profile.json` / `profile.json.gz` files
9
+ - turn Firefox Profiler data into compact, agent-friendly hotspot summaries
10
+ - drill down into specific threads and functions
11
+
12
+ The intended runtime is:
13
+
14
+ ```sh
15
+ npx mcp-samply
16
+ ```
17
+
18
+ ## Why this exists
19
+
20
+ `samply` is excellent at recording profiles and opening them in Firefox Profiler, but AI agents need a second layer:
21
+
22
+ - a stable MCP interface
23
+ - structured summaries instead of raw table-heavy profile JSON
24
+ - a way to work offline from saved profiles
25
+
26
+ `mcp-samply` provides that layer.
27
+
28
+ ## Tools
29
+
30
+ The server exposes eight MCP tools:
31
+
32
+ - `samply_doctor`: verify whether the `samply` binary is available and report version / environment details
33
+ - `samply_record`: run `samply record --save-only` and save a profile to disk
34
+ - `samply_summarize_profile`: generate a compact summary of threads, hotspots, markers, and overall sample distribution
35
+ - `samply_inspect_thread`: inspect one thread in detail, including representative stacks
36
+ - `samply_search_functions`: search for functions or library names across the loaded profile
37
+ - `samply_breakdown_subsystems`: group native functions by namespace prefix so agents can see which Rust / C++ subsystems dominate a profile
38
+ - `samply_focus_functions`: recover the most common caller / callee contexts around a target function or namespace
39
+ - `samply_locate_symbols`: map hot native symbols back to likely local source files so an agent can inspect implementation details
40
+
41
+ ## Presymbolication
42
+
43
+ By default, `samply_record` enables `--unstable-presymbolicate`.
44
+
45
+ That causes `samply` to emit a sidecar file such as:
46
+
47
+ ```text
48
+ profile.json.gz
49
+ profile.json.syms.json
50
+ ```
51
+
52
+ `mcp-samply` reads that sidecar automatically and uses it to resolve native addresses into function names during offline analysis. This is critical for AI-driven performance work; without it, saved profiles often contain only raw addresses.
53
+
54
+ ## Use With Codex
55
+
56
+ Example MCP server config:
57
+
58
+ ```json
59
+ {
60
+ "mcpServers": {
61
+ "samply": {
62
+ "command": "npx",
63
+ "args": ["-y", "mcp-samply"],
64
+ "env": {
65
+ "MCP_SAMPLY_BIN": "samply"
66
+ }
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ If `samply` is not on `PATH`, set `MCP_SAMPLY_BIN` to an absolute executable path.
73
+
74
+ You can still use the analysis tools on an existing profile file even when `samply` itself is not installed.
75
+
76
+ ## Typical Agent Workflow
77
+
78
+ 1. Call `samply_doctor`.
79
+ 2. Call `samply_record` with a command, PID, or `all=true`.
80
+ 3. Call `samply_summarize_profile` on the produced profile.
81
+ 4. Use `samply_inspect_thread` for the hottest thread.
82
+ 5. Use `samply_search_functions` for focused follow-up questions.
83
+ 6. Use `samply_breakdown_subsystems` to quantify native hotspots by crate / module prefix.
84
+ 7. Use `samply_focus_functions` to turn syscalls such as `stat` / `read` back into actionable upstream call paths.
85
+ 8. Use `samply_locate_symbols` to map the hottest native frames back to local source files before reading or patching code.
86
+
87
+ ## Local Development
88
+
89
+ ```sh
90
+ npm install
91
+ npm run build
92
+ npm test
93
+ ```
94
+
95
+ Run the MCP server locally:
96
+
97
+ ```sh
98
+ npm run dev
99
+ ```
100
+
101
+ Build output is published through the `mcp-samply` bin entry so the package can be launched with `npx`.
102
+
103
+ ## Publishing To npm
104
+
105
+ This repository includes a GitHub Actions workflow at `.github/workflows/publish-npm.yml`.
106
+
107
+ Before using it, add a repository secret named `NPM_TOKEN` with publish access to the `mcp-samply` package on npm.
108
+
109
+ The workflow can be triggered in two ways:
110
+
111
+ 1. Publish a GitHub Release whose tag matches the package version, for example `v0.1.0`.
112
+ 2. Run the workflow manually from the Actions tab with `workflow_dispatch` and provide the exact package version, for example `0.1.0`.
113
+
114
+ On every publish run, the workflow will:
115
+
116
+ 1. Install dependencies with `npm ci`.
117
+ 2. Run `npm run check`.
118
+ 3. Run `npm pack --dry-run`.
119
+ 4. Publish with `npm publish --provenance --access public`.
120
+
121
+ ## Debugging The MCP Surface
122
+
123
+ This repository also includes a small local debug client so you can inspect the MCP surface and call tools without wiring up an external MCP host first.
124
+
125
+ List the exposed MCP surfaces:
126
+
127
+ ```sh
128
+ npm run debug:mcp -- list-tools
129
+ ```
130
+
131
+ The output includes:
132
+
133
+ - `tools`: the currently registered MCP tools, including input and output schemas
134
+ - `prompts`: prompt definitions, if any
135
+ - `resources`: resource definitions, if any
136
+
137
+ Call a tool and print the full JSON result:
138
+
139
+ ```sh
140
+ npm run debug:mcp -- call samply_doctor
141
+ ```
142
+
143
+ Summarize one of the sample profiles committed in this repo:
144
+
145
+ ```sh
146
+ npm run debug:mcp -- call samply_summarize_profile --args '{"profilePath":".samply/presym-smoke.json.gz"}'
147
+ ```
148
+
149
+ Run symbol lookup against a local source tree:
150
+
151
+ ```sh
152
+ npm run debug:mcp -- call samply_locate_symbols --args '{"roots":["/absolute/path/to/project"],"symbols":["rspack::Compiler::build"],"extensions":[".rs",".cc",".cpp"]}'
153
+ ```
154
+
155
+ For larger payloads, store the tool input in a JSON file and use `--args-file`:
156
+
157
+ ```sh
158
+ npm run debug:mcp -- call samply_record --args-file ./tool-args.json
159
+ ```
160
+
161
+ Recommended manual debug loop:
162
+
163
+ 1. Run `npm run debug:mcp -- list-tools` to confirm the tool names and schemas.
164
+ 2. Call `samply_doctor` first to verify environment and binary resolution.
165
+ 3. Use `samply_record` or a saved `profile.json(.gz)` to generate real inputs.
166
+ 4. Run `samply_summarize_profile`, then drill into `samply_inspect_thread`, `samply_search_functions`, `samply_breakdown_subsystems`, and `samply_focus_functions`.
167
+ 5. Use `samply_locate_symbols` with your project root to confirm hotspot symbols map back to the files you expect.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { createServer } from "./server.js";
4
+ async function main() {
5
+ const server = createServer();
6
+ const transport = new StdioServerTransport();
7
+ await server.connect(transport);
8
+ console.error("mcp-samply is running on stdio");
9
+ }
10
+ main().catch((error) => {
11
+ console.error("mcp-samply failed to start:", error);
12
+ process.exit(1);
13
+ });
14
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAE7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;AAClD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAC;IACpD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,162 @@
1
+ import type { LoadedProfile } from "./store.js";
2
+ export interface HotFunctionSummary {
3
+ name: string;
4
+ resourceName: string | null;
5
+ displayName: string;
6
+ selfSamples: number;
7
+ stackSamples: number;
8
+ }
9
+ export interface MarkerSummary {
10
+ name: string;
11
+ count: number;
12
+ }
13
+ export interface ThreadSummary {
14
+ index: number;
15
+ name: string;
16
+ processName: string | null;
17
+ pid: number | null;
18
+ tid: number | null;
19
+ sampleCount: number;
20
+ startTimeMs: number | null;
21
+ endTimeMs: number | null;
22
+ durationMs: number | null;
23
+ topSelfFunctions: HotFunctionSummary[];
24
+ topStackFunctions: HotFunctionSummary[];
25
+ topMarkers: MarkerSummary[];
26
+ }
27
+ export interface ProfileSummaryResult {
28
+ profilePath: string;
29
+ sidecarPath: string | null;
30
+ presymbolicated: boolean;
31
+ product: string | null;
32
+ oscpu: string | null;
33
+ intervalMs: number | null;
34
+ processCount: number;
35
+ threadCount: number;
36
+ totalSamples: number;
37
+ sampleTimeRangeMs: number | null;
38
+ hottestSelfFunctionsOverall: HotFunctionSummary[];
39
+ hottestStackFunctionsOverall: HotFunctionSummary[];
40
+ threads: ThreadSummary[];
41
+ }
42
+ export interface ThreadInspectionResult {
43
+ profilePath: string;
44
+ sidecarPath: string | null;
45
+ presymbolicated: boolean;
46
+ thread: ThreadSummary & {
47
+ topStacks: Array<{
48
+ stack: string[];
49
+ sampleCount: number;
50
+ }>;
51
+ };
52
+ }
53
+ export interface FunctionSearchResult {
54
+ profilePath: string;
55
+ sidecarPath: string | null;
56
+ presymbolicated: boolean;
57
+ query: string;
58
+ matchCount: number;
59
+ matches: Array<HotFunctionSummary & {
60
+ threads: Array<{
61
+ index: number;
62
+ name: string;
63
+ processName: string | null;
64
+ selfSamples: number;
65
+ stackSamples: number;
66
+ }>;
67
+ }>;
68
+ }
69
+ export interface SubsystemBreakdownResult {
70
+ profilePath: string;
71
+ sidecarPath: string | null;
72
+ presymbolicated: boolean;
73
+ query: string | null;
74
+ resourceQuery: string | null;
75
+ prefixSegments: number;
76
+ groupCount: number;
77
+ totalGroupedStackSamples: number;
78
+ totalGroupedSelfSamples: number;
79
+ groups: Array<{
80
+ name: string;
81
+ selfSamples: number;
82
+ stackSamples: number;
83
+ exampleFunctions: HotFunctionSummary[];
84
+ threads: Array<{
85
+ index: number;
86
+ name: string;
87
+ processName: string | null;
88
+ selfSamples: number;
89
+ stackSamples: number;
90
+ }>;
91
+ }>;
92
+ }
93
+ export interface HotspotContextResult {
94
+ profilePath: string;
95
+ sidecarPath: string | null;
96
+ presymbolicated: boolean;
97
+ query: string;
98
+ resourceQuery: string | null;
99
+ totalMatchedSamples: number;
100
+ matchedFunctionCount: number;
101
+ matches: Array<{
102
+ name: string;
103
+ resourceName: string | null;
104
+ displayName: string;
105
+ sampleCount: number;
106
+ }>;
107
+ threads: Array<{
108
+ index: number;
109
+ name: string;
110
+ processName: string | null;
111
+ sampleCount: number;
112
+ }>;
113
+ topContexts: Array<{
114
+ match: string;
115
+ before: string[];
116
+ after: string[];
117
+ sampleCount: number;
118
+ }>;
119
+ topCallers: Array<{
120
+ match: string;
121
+ path: string[];
122
+ sampleCount: number;
123
+ }>;
124
+ topCallees: Array<{
125
+ match: string;
126
+ path: string[];
127
+ sampleCount: number;
128
+ }>;
129
+ }
130
+ export declare function summarizeProfile(profile: LoadedProfile, options?: {
131
+ maxThreads?: number | undefined;
132
+ maxFunctions?: number | undefined;
133
+ maxMarkers?: number | undefined;
134
+ includeEmptyThreads?: boolean | undefined;
135
+ }): ProfileSummaryResult;
136
+ export declare function inspectThread(profile: LoadedProfile, selector: number | string, options?: {
137
+ maxFunctions?: number | undefined;
138
+ maxMarkers?: number | undefined;
139
+ maxStacks?: number | undefined;
140
+ }): ThreadInspectionResult;
141
+ export declare function searchFunctions(profile: LoadedProfile, query: string, options?: {
142
+ maxResults?: number | undefined;
143
+ maxThreadsPerResult?: number | undefined;
144
+ }): FunctionSearchResult;
145
+ export declare function breakdownBySubsystem(profile: LoadedProfile, options?: {
146
+ query?: string | undefined;
147
+ resourceQuery?: string | undefined;
148
+ thread?: number | string | undefined;
149
+ prefixSegments?: number | undefined;
150
+ maxGroups?: number | undefined;
151
+ maxExamplesPerGroup?: number | undefined;
152
+ maxThreadsPerGroup?: number | undefined;
153
+ }): SubsystemBreakdownResult;
154
+ export declare function focusFunctions(profile: LoadedProfile, query: string, options?: {
155
+ resourceQuery?: string | undefined;
156
+ thread?: number | string | undefined;
157
+ beforeDepth?: number | undefined;
158
+ afterDepth?: number | undefined;
159
+ maxMatches?: number | undefined;
160
+ maxThreads?: number | undefined;
161
+ maxContexts?: number | undefined;
162
+ }): HotspotContextResult;