Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions apps/mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# hackproof-mcp

Model Context Protocol (MCP) server for [HackProof](https://github.com/devodii/hackproof) — inspect, query, and verify HackMeridian 2026 hackathon projects, contracts, and deployments rebuilt from Stellar testnet ledger activity.

## Features & Tools

- `list_hackathons`: List indexed hackathons and verification windows.
- `get_summary`: High-level aggregate metrics (projects, contracts, builders, freshness timestamp).
- `query_projects`: Search projects by keywords, evidence tier, track, or integration partner.
- `get_project`: Full project breakdown including contracts, wasm uploads, and evidence links.
- `get_contract`: Detailed contract deployment record on Stellar testnet.
- `hourly`: 27-hour window contract deployment timeline.
- `distributions`: Geographic countries and tier distributions.
- `top_errors`: Most frequent transaction failure codes and error kinds.
- `compare_idea(text)`: Semantic keyword similarity matcher against all submissions.
- `methodology()`: Official evidence tier definitions and attribution rules.

Every tool response includes a `generated_at` timestamp indicating data freshness. Responses are cached with an in-memory TTL and support ETag revalidation.

---

## Installation & Running

You can run the MCP server directly using `npx`:

```sh
npx hackproof-mcp
```

Or run locally within the HackProof repository:

```sh
cd apps/mcp
pnpm build
pnpm start
```

---

## Configuration

### Claude Desktop

Add this to your `claude_desktop_config.json`:

- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
- **Linux:** `~/.config/Claude/claude_desktop_config.json`

```json
{
"mcpServers": {
"hackproof": {
"command": "npx",
"args": ["-y", "hackproof-mcp"],
"env": {
"HACKPROOF_API_URL": "https://hackproof.vercel.app/api"
}
}
}
}
```

### Cursor

Add to your project's `.cursor/mcp.json` or global Cursor settings:

```json
{
"mcpServers": {
"hackproof": {
"command": "npx",
"args": ["-y", "hackproof-mcp"],
"env": {
"HACKPROOF_API_URL": "https://hackproof.vercel.app/api"
}
}
}
}
```

### Raven

Add to your `raven.json` MCP configuration:

```json
{
"servers": [
{
"name": "hackproof",
"transport": "stdio",
"command": "npx",
"args": ["-y", "hackproof-mcp"],
"env": {
"HACKPROOF_API_URL": "https://hackproof.vercel.app/api"
}
}
]
}
```

---

## Development & Tests

```sh
cd apps/mcp
pnpm install
pnpm build
pnpm test
```
37 changes: 37 additions & 0 deletions apps/mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "hackproof-mcp",
"version": "0.1.0",
"description": "MCP server for HackProof - inspect and query hackathon projects and contracts rebuilt from Stellar testnet",
"type": "module",
"bin": {
"hackproof-mcp": "./dist/index.js"
},
"main": "./dist/index.js",
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"start": "node ./dist/index.js",
"test": "node --test dist/**/*.test.js"
},
"keywords": [
"mcp",
"model-context-protocol",
"stellar",
"soroban",
"hackproof",
"hackathon"
],
"author": "HackProof Contributors",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.1"
},
"devDependencies": {
"@types/node": "^24.19.2",
"typescript": "~6.0.3"
}
}
180 changes: 180 additions & 0 deletions apps/mcp/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import type {
Contract,
CountryCount,
FailedTx,
Integration,
Page,
Project,
ProjectDetail,
Stats,
TimelinePoint,
WasmUpload,
} from './types.js'

interface CacheEntry<T> {
data: T
etag?: string
expiresAt: number
generatedAt: string
}

export interface ApiResponseWithFreshness<T> {
data: T
generated_at: string
}

export class HackProofClient {
private apiBase: string
private cache: Map<string, CacheEntry<any>> = new Map()
private defaultTtlMs: number = 60_000 // 60s in-memory cache

constructor(apiBase?: string) {
this.apiBase = (
apiBase ||
process.env.HACKPROOF_API_URL ||
'https://hackproof.vercel.app/api'
).replace(/\/$/, '')
}

private async fetchWithCache<T>(path: string): Promise<ApiResponseWithFreshness<T>> {
const url = `${this.apiBase}${path.startsWith('/') ? path : `/${path}`}`
const now = Date.now()
const cached = this.cache.get(url)

const headers: Record<string, string> = {
Accept: 'application/json',
'User-Agent': 'HackProof-MCP-Server/0.1.0',
}

if (cached) {
if (now < cached.expiresAt) {
return { data: cached.data, generated_at: cached.generatedAt }
}
if (cached.etag) {
headers['If-None-Match'] = cached.etag
}
}

try {
const resp = await fetch(url, { headers })

if (resp.status === 304 && cached) {
cached.expiresAt = now + this.defaultTtlMs
return { data: cached.data, generated_at: cached.generatedAt }
}

if (!resp.ok) {
throw new Error(`API error from ${url}: ${resp.status} ${resp.statusText}`)
}

const etag = resp.headers.get('ETag') || undefined
const dateHeader = resp.headers.get('Date')
const generatedAt = dateHeader
? new Date(dateHeader).toISOString()
: new Date(now).toISOString()

const data = (await resp.json()) as T

this.cache.set(url, {
data,
etag,
expiresAt: now + this.defaultTtlMs,
generatedAt,
})

return { data, generated_at: generatedAt }
} catch (err) {
// If network fails but we have stale cache, serve stale
if (cached) {
return { data: cached.data, generated_at: cached.generatedAt }
}
throw err
}
}

async getStats(): Promise<ApiResponseWithFreshness<Stats>> {
const res = await this.fetchWithCache<Stats>('/stats')
const stats = res.data
const generatedAt = stats.generated_at || res.generated_at
return { data: { ...stats, generated_at: generatedAt }, generated_at: generatedAt }
}

async getTimeline(): Promise<ApiResponseWithFreshness<TimelinePoint[]>> {
return this.fetchWithCache<TimelinePoint[]>('/timeline')
}

async getCountries(): Promise<ApiResponseWithFreshness<CountryCount[]>> {
return this.fetchWithCache<CountryCount[]>('/countries')
}

async getIntegrations(): Promise<ApiResponseWithFreshness<Integration[]>> {
return this.fetchWithCache<Integration[]>('/integrations')
}

async getProjects(params?: {
q?: string
tier?: string
track?: string
integration?: string
limit?: number
offset?: number
}): Promise<ApiResponseWithFreshness<Page<Project>>> {
const query = new URLSearchParams()
if (params?.q) query.set('q', params.q)
if (params?.tier) query.set('tier', params.tier)
if (params?.track) query.set('track', params.track)
if (params?.integration) query.set('integration', params.integration)
if (params?.limit) query.set('limit', params.limit.toString())
if (params?.offset) query.set('offset', params.offset.toString())

const qs = query.toString()
const path = `/projects${qs ? `?${qs}` : ''}`
return this.fetchWithCache<Page<Project>>(path)
}

async getProject(projectId: string): Promise<ApiResponseWithFreshness<ProjectDetail>> {
return this.fetchWithCache<ProjectDetail>(`/projects/${encodeURIComponent(projectId)}`)
}

async getContracts(params?: {
q?: string
limit?: number
offset?: number
}): Promise<ApiResponseWithFreshness<Page<Contract>>> {
const query = new URLSearchParams()
if (params?.q) query.set('q', params.q)
if (params?.limit) query.set('limit', params.limit.toString())
if (params?.offset) query.set('offset', params.offset.toString())

const qs = query.toString()
return this.fetchWithCache<Page<Contract>>(`/contracts${qs ? `?${qs}` : ''}`)
}

async getWasm(params?: {
q?: string
limit?: number
offset?: number
}): Promise<ApiResponseWithFreshness<Page<WasmUpload>>> {
const query = new URLSearchParams()
if (params?.q) query.set('q', params.q)
if (params?.limit) query.set('limit', params.limit.toString())
if (params?.offset) query.set('offset', params.offset.toString())

const qs = query.toString()
return this.fetchWithCache<Page<WasmUpload>>(`/wasm${qs ? `?${qs}` : ''}`)
}

async getFailed(params?: {
q?: string
limit?: number
offset?: number
}): Promise<ApiResponseWithFreshness<Page<FailedTx>>> {
const query = new URLSearchParams()
if (params?.q) query.set('q', params.q)
if (params?.limit) query.set('limit', params.limit.toString())
if (params?.offset) query.set('offset', params.offset.toString())

const qs = query.toString()
return this.fetchWithCache<Page<FailedTx>>(`/failed${qs ? `?${qs}` : ''}`)
}
}
27 changes: 27 additions & 0 deletions apps/mcp/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env node

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { createMcpServer } from './server.js'

async function main() {
const server = createMcpServer()
const transport = new StdioServerTransport()

process.on('SIGINT', async () => {
await server.close()
process.exit(0)
})

process.on('SIGTERM', async () => {
await server.close()
process.exit(0)
})

await server.connect(transport)
console.error('HackProof MCP server running on stdio')
}

main().catch((error) => {
console.error('Fatal error in HackProof MCP server:', error)
process.exit(1)
})
Loading