Sign In

tracetify-mcp

Package Overview
Dependencies
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

tracetify-mcp - npm Package Compare versions

Comparing version
0.0.1
to
0.1.0
+78
src/server.mjs
/**
* 薄客户端:只做 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 = '';
const DIM = '';
const AMBER = '';
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());
{
"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