tracetify-mcp
Advanced tools
| /** | ||
| * 薄客户端:只做 MCP ↔ Tracetify REST 的协议转换,不含任何业务逻辑。 | ||
| * 编排、防坑规则、分析层全在服务端(MCP spec 架构决定 4)。 | ||
| * createServer 把 fetch 做成注入点——测试不用起网络。 | ||
| */ | ||
| import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; | ||
| import { z } from 'zod'; | ||
| const DEFAULT_BASE = 'https://tracetify.com'; | ||
| export function createServer({ apiKey, baseUrl = DEFAULT_BASE, fetchImpl = fetch } = {}) { | ||
| const server = new McpServer({ name: 'tracetify', version: '0.1.0' }); | ||
| async function call(path, init = {}) { | ||
| const res = await fetchImpl(`${baseUrl}${path}`, { | ||
| ...init, | ||
| headers: { | ||
| Authorization: `Bearer ${apiKey}`, | ||
| 'Content-Type': 'application/json', | ||
| ...init.headers, | ||
| }, | ||
| }); | ||
| const body = await res.json().catch(() => ({})); | ||
| if (!res.ok) throw new Error(body.error || `Tracetify API error (HTTP ${res.status})`); | ||
| return body; | ||
| } | ||
| const text = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }); | ||
| server.registerTool( | ||
| 'search_reports', | ||
| { | ||
| description: | ||
| 'Search existing Tracetify growth reports by domain. Reading existing reports is free.', | ||
| inputSchema: { query: z.string().describe('Domain or fragment, e.g. "weshop" or "weshop.ai"') }, | ||
| }, | ||
| async ({ query }) => text(await call(`/api/mcp/v1/reports/search?q=${encodeURIComponent(query)}`)) | ||
| ); | ||
| server.registerTool( | ||
| 'read_report', | ||
| { | ||
| description: | ||
| 'Read one Tracetify growth report by slug (from search_reports or a completed trace). Free.', | ||
| inputSchema: { slug: z.string() }, | ||
| }, | ||
| async ({ slug }) => text(await call(`/api/mcp/v1/reports/${encodeURIComponent(slug)}`)) | ||
| ); | ||
| server.registerTool( | ||
| 'start_trace', | ||
| { | ||
| description: | ||
| 'Trace how a product actually grew: 12 sources, takes 60-90s, costs credits from your Tracetify balance. Returns a cached report slug for free when a fresh one already exists. Poll progress with get_trace.', | ||
| inputSchema: { | ||
| url: z.string().describe('Domain to trace, e.g. weshop.ai'), | ||
| refresh: z.boolean().optional().describe('Force a fresh run even if a cached report exists'), | ||
| }, | ||
| }, | ||
| async ({ url, refresh }) => | ||
| text(await call('/api/mcp/v1/trace', { | ||
| method: 'POST', | ||
| body: JSON.stringify({ url, refresh: refresh === true }), | ||
| })) | ||
| ); | ||
| server.registerTool( | ||
| 'get_trace', | ||
| { | ||
| description: 'Check a running trace. When status is "done", read the report with read_report.', | ||
| inputSchema: { job_id: z.string() }, | ||
| }, | ||
| async ({ job_id }) => text(await call(`/api/mcp/v1/trace/${encodeURIComponent(job_id)}`)) | ||
| ); | ||
| return server; | ||
| } |
| import { describe, expect, it, vi } from 'vitest'; | ||
| import { Client } from '@modelcontextprotocol/sdk/client/index.js'; | ||
| import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; | ||
| import { createServer } from './server.mjs'; | ||
| async function connected(fetchImpl) { | ||
| const server = createServer({ apiKey: 'ttfy_test', baseUrl: 'https://api.test', fetchImpl }); | ||
| const client = new Client({ name: 'test', version: '0.0.0' }); | ||
| const [a, b] = InMemoryTransport.createLinkedPair(); | ||
| await Promise.all([server.connect(a), client.connect(b)]); | ||
| return client; | ||
| } | ||
| describe('tracetify-mcp server', () => { | ||
| it('exposes the four phase-1 tools', async () => { | ||
| const client = await connected(vi.fn()); | ||
| const { tools } = await client.listTools(); | ||
| expect(tools.map((t) => t.name).sort()).toEqual( | ||
| ['get_trace', 'read_report', 'search_reports', 'start_trace'] | ||
| ); | ||
| }); | ||
| it('search_reports hits the REST API with the key and returns its JSON', async () => { | ||
| const fetchImpl = vi.fn().mockResolvedValue({ | ||
| ok: true, | ||
| json: async () => ({ reports: [{ slug: 'weshop-ai' }] }), | ||
| }); | ||
| const client = await connected(fetchImpl); | ||
| const res = await client.callTool({ name: 'search_reports', arguments: { query: 'weshop' } }); | ||
| expect(fetchImpl).toHaveBeenCalledWith( | ||
| 'https://api.test/api/mcp/v1/reports/search?q=weshop', | ||
| expect.objectContaining({ | ||
| headers: expect.objectContaining({ Authorization: 'Bearer ttfy_test' }), | ||
| }) | ||
| ); | ||
| expect(res.content[0].text).toContain('weshop-ai'); | ||
| }); | ||
| it('start_trace posts url + refresh', async () => { | ||
| const fetchImpl = vi.fn().mockResolvedValue({ | ||
| ok: true, | ||
| json: async () => ({ status: 'started', jobId: 'j1' }), | ||
| }); | ||
| const client = await connected(fetchImpl); | ||
| await client.callTool({ name: 'start_trace', arguments: { url: 'weshop.ai', refresh: true } }); | ||
| expect(fetchImpl).toHaveBeenCalledWith( | ||
| 'https://api.test/api/mcp/v1/trace', | ||
| expect.objectContaining({ method: 'POST', body: JSON.stringify({ url: 'weshop.ai', refresh: true }) }) | ||
| ); | ||
| }); | ||
| it('surfaces API errors as tool errors instead of fake success', async () => { | ||
| const fetchImpl = vi.fn().mockResolvedValue({ | ||
| ok: false, | ||
| status: 402, | ||
| json: async () => ({ error: 'A trace costs 10 credits' }), | ||
| }); | ||
| const client = await connected(fetchImpl); | ||
| const res = await client.callTool({ name: 'start_trace', arguments: { url: 'weshop.ai' } }); | ||
| expect(res.isError).toBe(true); | ||
| expect(res.content[0].text).toContain('A trace costs 10 credits'); | ||
| }); | ||
| }); |
+15
-27
| #!/usr/bin/env node | ||
| import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; | ||
| import { createServer } from '../src/server.mjs'; | ||
| /** | ||
| * 0.0.1 占位版。真正的 MCP server 还没实现——这个可执行文件只负责说清楚 | ||
| * 现状并退出,绝不假装自己能连上 Claude Code。 | ||
| * | ||
| * 装了它的人多半是在 MCP 目录里刷到的,此刻最有用的信息是「还没好, | ||
| * 网站现在就能用」,而不是一个握手到一半失败的进程。 | ||
| */ | ||
| const apiKey = process.env.TRACETIFY_API_KEY; | ||
| if (!apiKey) { | ||
| console.error( | ||
| 'TRACETIFY_API_KEY is not set.\n' | ||
| + 'Create a key at https://tracetify.com/dashboard and add it to your MCP client config.' | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| const RESET = '[0m'; | ||
| const DIM = '[2m'; | ||
| const AMBER = '[33m'; | ||
| const lines = [ | ||
| '', | ||
| ` ${AMBER}tracetify-mcp${RESET} ${DIM}v0.0.1 — placeholder, not functional yet${RESET}`, | ||
| '', | ||
| ' Tracetify reconstructs how a product actually grew: the first mention,', | ||
| ' the quiet weeks, the directory wave, the launch spike — every step dated', | ||
| ' and linked to the page it came from.', | ||
| '', | ||
| ' The MCP server that exposes this to Claude Code and Cursor is being built.', | ||
| ` Until it ships, everything works on the web: ${AMBER}https://tracetify.com${RESET}`, | ||
| '', | ||
| ` ${DIM}Watch this package for the first working release.${RESET}`, | ||
| '', | ||
| ]; | ||
| process.stdout.write(lines.join('\n') + '\n'); | ||
| const server = createServer({ | ||
| apiKey, | ||
| baseUrl: process.env.TRACETIFY_API_URL || 'https://tracetify.com', | ||
| }); | ||
| await server.connect(new StdioServerTransport()); |
+13
-20
| { | ||
| "name": "tracetify-mcp", | ||
| "version": "0.0.1", | ||
| "version": "0.1.0", | ||
| "description": "MCP server for Tracetify — trace how any product actually grew, from inside Claude Code or Cursor.", | ||
| "keywords": [ | ||
| "mcp", | ||
| "modelcontextprotocol", | ||
| "model-context-protocol", | ||
| "seo", | ||
| "competitive-analysis", | ||
| "competitor-research", | ||
| "growth", | ||
| "tracetify" | ||
| "mcp", "modelcontextprotocol", "model-context-protocol", | ||
| "seo", "competitive-analysis", "competitor-research", "growth", "tracetify" | ||
| ], | ||
@@ -19,15 +13,14 @@ "homepage": "https://tracetify.com", | ||
| "type": "module", | ||
| "bin": { | ||
| "tracetify-mcp": "bin/tracetify-mcp.mjs" | ||
| "bin": { "tracetify-mcp": "bin/tracetify-mcp.mjs" }, | ||
| "files": ["bin", "src", "README.md"], | ||
| "engines": { "node": ">=20" }, | ||
| "publishConfig": { "access": "public" }, | ||
| "scripts": { "test": "vitest run" }, | ||
| "dependencies": { | ||
| "@modelcontextprotocol/sdk": "^1.12.0", | ||
| "zod": "^3.24.0" | ||
| }, | ||
| "files": [ | ||
| "bin", | ||
| "README.md" | ||
| ], | ||
| "engines": { | ||
| "node": ">=20" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public" | ||
| "devDependencies": { | ||
| "vitest": "^3.2.0" | ||
| } | ||
| } |
+28
-24
| # tracetify-mcp | ||
| > **Status: placeholder (v0.0.1). Not functional yet.** | ||
| > The package name is reserved while the server is being built. Nothing here | ||
| > connects to an MCP client today. | ||
| MCP server for [Tracetify](https://tracetify.com) — trace how any product | ||
| actually grew, without leaving Claude Code or Cursor. | ||
| ## What Tracetify does | ||
| Most competitive tools tell you where a product stands **today**. Tracetify | ||
| reconstructs **how it got there**: the first mention, the quiet weeks, the | ||
| directory wave, the launch spike. Twelve sources per trace, every claim linked | ||
| to the page it came from. | ||
| directory wave, the launch spike. Twelve sources per trace, every claim | ||
| linked to the page it came from. | ||
| ## What this package will expose | ||
| ## Setup | ||
| Planned for the first working release: | ||
| 1. Sign in at [tracetify.com](https://tracetify.com) and create an API key | ||
| in the dashboard. | ||
| 2. Add the server to your MCP client config: | ||
| | Tool | What it does | | ||
| | --- | --- | | ||
| | `reports.search` / `reports.read` | Read any report that has already been traced — free | | ||
| | `trace.start` / `trace.get` | Trace a competitor from twelve sources | | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "tracetify": { | ||
| "command": "npx", | ||
| "args": ["-y", "tracetify-mcp"], | ||
| "env": { "TRACETIFY_API_KEY": "ttfy_..." } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| Later: AI-answer visibility, site audits, backlink and domain research, and | ||
| your own Search Console data. | ||
| Claude Code: `claude mcp add tracetify -e TRACETIFY_API_KEY=ttfy_... -- npx -y tracetify-mcp` | ||
| Reading existing reports is free. Running a fresh trace draws from your | ||
| Tracetify credit balance — the same balance the website uses, no seats and no | ||
| per-tool add-ons. | ||
| ## Tools | ||
| ## Install | ||
| | Tool | Cost | What it does | | ||
| | --- | --- | --- | | ||
| | `search_reports` | free | Find existing growth reports by domain | | ||
| | `read_report` | free | Read a full report (timeline & verdict follow your account's unlocks) | | ||
| | `start_trace` | credits | Trace a new competitor from 12 sources (~60–90s); returns a cached report for free when a fresh one exists | | ||
| | `get_trace` | free | Poll a running trace | | ||
| Not yet. When the first working release lands, this section will carry the | ||
| config snippet for Claude Code, Claude Desktop, and Cursor. | ||
| Fresh traces draw from your Tracetify credit balance — the same balance the | ||
| website uses. No seats, no per-tool add-ons. Top up at | ||
| [tracetify.com/pricing](https://tracetify.com/pricing). | ||
| Until then, everything works on the web: **<https://tracetify.com>** | ||
| ## License | ||
| MIT |
Network access
Supply chain riskThis module accesses the network.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
8187
161.9%5
66.67%142
446.15%1
-50%47
9.3%2
Infinity%1
Infinity%4
100%1
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added