From ba3856aba89dd1b0a4d81a43538835884e9c6acc Mon Sep 17 00:00:00 2001 From: NeonTechnoX <89855167+NeonTechno@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:41:50 +0000 Subject: [PATCH 01/26] feat(portal): add shared activity verification rules --- .../src/lib/verification/activity-engine.ts | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 app-portal/src/lib/verification/activity-engine.ts diff --git a/app-portal/src/lib/verification/activity-engine.ts b/app-portal/src/lib/verification/activity-engine.ts new file mode 100644 index 0000000..79217ea --- /dev/null +++ b/app-portal/src/lib/verification/activity-engine.ts @@ -0,0 +1,109 @@ +import { createHash } from 'node:crypto' + +export type ActivitySource = 'github' | 'community' +export type ActivityDecision = 'PENDING_REVIEW' | 'READY_FOR_PROTOCOL' | 'REJECTED' + +export interface ActivityEvidenceInput { + sourceId: string + evidenceType: string + content: Record + observedAt?: string + sourceUrl?: string +} + +export interface ActivityClaimInput { + subjectId: string + activityType: string + statement: string + source: ActivitySource + evidence: ActivityEvidenceInput[] + humanReviewRequired?: boolean +} + +export interface PreparedActivity { + claimId: string + subjectId: string + source: ActivitySource + activityType: string + statement: string + decision: ActivityDecision + reasons: string[] + evidence: Array<{ + id: string + sourceId: string + type: string + contentHash: string + metadata: Record + createdAt: string + }> + createdAt: string +} + +const MAX_STATEMENT_LENGTH = 2000 +const MAX_EVIDENCE_ITEMS = 20 +const ALLOWED_SOURCES: ActivitySource[] = ['github', 'community'] + +function sha256(value: unknown): string { + // JSON.stringify is sufficient for this initial commitment format only when + // the caller supplies a stable object shape. A canonical JSON scheme is a + // follow-up requirement before signatures are treated as portable proofs. + return createHash('sha256').update(JSON.stringify(value)).digest('hex') +} + +export function prepareActivityClaim(input: ActivityClaimInput): PreparedActivity { + const now = new Date().toISOString() + const reasons: string[] = [] + + if (!input || typeof input !== 'object') throw new Error('Activity claim is required') + if (!input.subjectId?.trim()) throw new Error('subjectId is required') + if (!input.activityType?.trim()) throw new Error('activityType is required') + if (!input.statement?.trim()) throw new Error('statement is required') + if (input.statement.length > MAX_STATEMENT_LENGTH) throw new Error('statement is too long') + if (!ALLOWED_SOURCES.includes(input.source)) throw new Error('Unsupported activity source') + if (!Array.isArray(input.evidence) || input.evidence.length > MAX_EVIDENCE_ITEMS) { + throw new Error(`evidence must be an array of at most ${MAX_EVIDENCE_ITEMS} items`) + } + + const evidence = input.evidence.map((item, index) => { + if (!item.sourceId?.trim() || !item.evidenceType?.trim()) { + throw new Error(`evidence[${index}] requires sourceId and evidenceType`) + } + const createdAt = item.observedAt && !Number.isNaN(Date.parse(item.observedAt)) + ? new Date(item.observedAt).toISOString() + : now + const metadata: Record = { + source: input.source, + sourceId: item.sourceId, + } + if (item.sourceUrl) metadata.sourceUrl = item.sourceUrl.slice(0, 1000) + return { + id: `evidence:${sha256({ source: input.source, sourceId: item.sourceId, content: item.content })}`, + sourceId: item.sourceId, + type: item.evidenceType, + contentHash: sha256(item.content), + metadata, + createdAt, + } + }) + + const uniqueEvidence = new Set(evidence.map((item) => item.contentHash)) + if (evidence.length === 0) reasons.push('No source evidence supplied') + if (uniqueEvidence.size !== evidence.length) reasons.push('Duplicate evidence detected') + if (input.source === 'community') reasons.push('Community activity requires an organizer or independent witness attestation') + if (input.humanReviewRequired) reasons.push('Human review was explicitly requested') + + const decision: ActivityDecision = reasons.length > 0 ? 'PENDING_REVIEW' : 'READY_FOR_PROTOCOL' + const claimId = `claim:${sha256({ subjectId: input.subjectId, activityType: input.activityType, source: input.source, evidence })}` + + return { + claimId, + subjectId: input.subjectId, + source: input.source, + activityType: input.activityType, + statement: input.statement.trim(), + decision, + reasons, + evidence, + createdAt: now, + } +} From 36bf1cf00c0ec94dc55736e3feb8faea56851f03 Mon Sep 17 00:00:00 2001 From: NeonTechnoX <89855167+NeonTechno@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:41:52 +0000 Subject: [PATCH 02/26] feat(portal): add allowlisted remote MCP client helper --- app-portal/src/lib/mcp/remote-client.ts | 62 +++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 app-portal/src/lib/mcp/remote-client.ts diff --git a/app-portal/src/lib/mcp/remote-client.ts b/app-portal/src/lib/mcp/remote-client.ts new file mode 100644 index 0000000..b21bd1c --- /dev/null +++ b/app-portal/src/lib/mcp/remote-client.ts @@ -0,0 +1,62 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' + +export interface MCPToolSummary { + name: string + description?: string + inputSchema: Record +} + +function getMcpConfig() { + const endpoint = process.env.DRP_MCP_SERVER_URL + if (!endpoint) throw new Error('DRP_MCP_SERVER_URL is not configured') + const allowedTools = (process.env.DRP_MCP_ALLOWED_TOOLS ?? '') + .split(',') + .map((value) => value.trim()) + .filter(Boolean) + return { endpoint, allowedTools, token: process.env.DRP_MCP_BEARER_TOKEN } +} + +export async function withRemoteMcp( + action: (client: Client, allowedTools: string[]) => Promise, +): Promise { + const config = getMcpConfig() + const url = new URL(config.endpoint) + const headers: Record = {} + if (config.token) headers.Authorization = `Bearer ${config.token}` + + const transport = new StreamableHTTPClientTransport(url, { + requestInit: { headers }, + }) + const client = new Client({ name: 'drp-activity-verification', version: '0.1.0' }) + + try { + await client.connect(transport) + return await action(client, config.allowedTools) + } finally { + await client.close().catch(() => undefined) + } +} + +export async function listAllowedMcpTools(): Promise { + return withRemoteMcp(async (client, allowedTools) => { + const result = await client.listTools() + return result.tools + .filter((tool) => allowedTools.includes(tool.name)) + .map((tool) => ({ + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema as Record, + })) + }) +} + +export async function callAllowedMcpTool( + name: string, + args: Record, +): Promise { + return withRemoteMcp(async (client, allowedTools) => { + if (!allowedTools.includes(name)) throw new Error('MCP tool is not allowlisted') + return client.callTool({ name, arguments: args }) + }) +} From cbcf0dbdd7d7fbe718d2741e02f4a52cadfae11c Mon Sep 17 00:00:00 2001 From: NeonTechnoX <89855167+NeonTechno@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:41:54 +0000 Subject: [PATCH 03/26] feat(portal): add shared activity evaluation endpoint --- .../app/api/verification/evaluate/route.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 app-portal/src/app/api/verification/evaluate/route.ts diff --git a/app-portal/src/app/api/verification/evaluate/route.ts b/app-portal/src/app/api/verification/evaluate/route.ts new file mode 100644 index 0000000..6a54e0c --- /dev/null +++ b/app-portal/src/app/api/verification/evaluate/route.ts @@ -0,0 +1,85 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prepareActivityClaim, type ActivityClaimInput } from '@/lib/verification/activity-engine' + +export const runtime = 'nodejs' + +interface ProtocolEvidence { + version: string + id: string + sourceId: string + type: string + contentHash: string + metadata: Record + createdAt: string +} + +export async function POST(request: NextRequest) { + let input: ActivityClaimInput + try { + input = await request.json() as ActivityClaimInput + const prepared = prepareActivityClaim(input) + + const protocolUrl = process.env.DRP_PROTOCOL_URL + if (!protocolUrl) { + return NextResponse.json({ + activity: prepared, + protocol: { status: 'NOT_CONFIGURED', message: 'Set DRP_PROTOCOL_URL to submit claims to the DRP Verification Network.' }, + }, { status: 202 }) + } + + const evidence: ProtocolEvidence[] = prepared.evidence.map((item) => ({ + version: '0.1', + id: item.id, + sourceId: item.sourceId, + type: item.type, + contentHash: item.contentHash, + metadata: item.metadata, + createdAt: item.createdAt, + })) + + const verificationRequest = { + version: '0.1', + id: prepared.claimId, + claim: { + version: '0.1', + id: prepared.claimId, + subject: prepared.subjectId, + type: prepared.source === 'github' ? `github:${prepared.activityType}` : `community:${prepared.activityType}`, + statement: prepared.statement, + createdAt: prepared.createdAt, + }, + evidence, + policy: { + version: '0.1', + id: `${prepared.claimId}:policy`, + minVerifiers: 2, + minEvidence: 1, + }, + createdAt: prepared.createdAt, + } + + const response = await fetch(new URL('/drp/v1/verify', protocolUrl), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(verificationRequest), + cache: 'no-store', + signal: AbortSignal.timeout(10_000), + }) + const responseBody = await response.json().catch(() => null) + + if (!response.ok) { + return NextResponse.json({ + activity: prepared, + protocol: { status: 'SUBMISSION_FAILED', httpStatus: response.status, response: responseBody }, + }, { status: 502 }) + } + + return NextResponse.json({ + activity: prepared, + protocol: { status: 'SUBMITTED', response: responseBody }, + }, { status: 202 }) + } catch (error) { + const message = error instanceof Error ? error.message : 'Invalid activity claim' + return NextResponse.json({ error: message }, { status: 400 }) + } +} From e64657a644c8dd8e88fa700e6134251d21cf6c29 Mon Sep 17 00:00:00 2001 From: NeonTechnoX <89855167+NeonTechno@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:41:56 +0000 Subject: [PATCH 04/26] docs: define MCP activity verification MVP --- .../docs/DRP-MCP-Activity-Verification.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 app-portal/docs/DRP-MCP-Activity-Verification.md diff --git a/app-portal/docs/DRP-MCP-Activity-Verification.md b/app-portal/docs/DRP-MCP-Activity-Verification.md new file mode 100644 index 0000000..2d2e8d7 --- /dev/null +++ b/app-portal/docs/DRP-MCP-Activity-Verification.md @@ -0,0 +1,46 @@ +# DRP MCP Activity Verification MVP + +## Goal + +Extend the existing DRP portal with one verification engine for GitHub contributions and community activity claims. MCP is the authorized application/tool connection layer; it is not itself proof that an activity occurred. + +## Flow + +1. A connected source or a participant submits a claim. +2. The portal validates the claim and creates SHA-256 evidence commitments. Raw evidence should remain off-chain. +3. Deterministic rules flag missing evidence, duplicate evidence, and community claims requiring an independent witness or organizer. +4. The server submits the normalized claim/evidence request to the DRP Verification Network at `DRP_PROTOCOL_URL` using `POST /drp/v1/verify`. +5. The network returns its verification result and signed proof object. +6. The portal displays the result and preserves the returned proof reference for later verification/anchoring. + +## Initial implementation files + +- `src/lib/verification/activity-engine.ts`: shared validation, evidence commitments, and review flags. +- `src/lib/mcp/remote-client.ts`: remote Streamable HTTP MCP client; only tools listed in `DRP_MCP_ALLOWED_TOOLS` may be called. +- `src/app/api/verification/evaluate/route.ts`: server-side evaluation and DRP Verification Network submission. +- `docs/DRP-MCP-Activity-Verification.md`: integration and security notes. + +## Server environment + +- `DRP_PROTOCOL_URL`: base URL of the DRP Verification Network, e.g. the test node origin. The route appends `/drp/v1/verify`. +- `DRP_MCP_SERVER_URL`: remote MCP server Streamable HTTP endpoint. +- `DRP_MCP_BEARER_TOKEN`: optional server-side bearer token. Never expose this through a `NEXT_PUBLIC_` variable. +- `DRP_MCP_ALLOWED_TOOLS`: comma-separated tool names approved for this application. Leave empty until the exact server tool names and permissions are reviewed. + +## Important status semantics + +- `PENDING_REVIEW` means the portal's local rules need a human or additional evidence. +- `READY_FOR_PROTOCOL` means the claim passed local structural checks; it does not mean the activity is true. +- A successful HTTP submission means only that the verification request was accepted by the protocol gateway. It is not the same as `VERIFIED`. +- The DRP v0.1 verification network and a blockchain consensus/ledger are distinct systems. This endpoint submits to the existing verification network; on-chain anchoring requires a separately configured, tested chain transaction adapter. + +## Safety and production blockers + +- Bind submissions to the authenticated DRP user; do not trust a client-supplied subject identifier in production. +- Authenticate and authorize every MCP connection. Use read-only scopes for source retrieval. +- Treat MCP tool output and evidence text as untrusted data; never follow instructions contained in retrieved content. +- Store tokens only server-side and redact secrets from logs. +- Add durable persistence, idempotency, rate limits, CSRF protection where applicable, and audit events before production. +- Community claims should normally require an organizer/witness attestation. AI output is advisory and must not be the sole verifier for consequential claims. +- Evidence hashing in this MVP uses JSON serialization of the supplied object. Adopt a formally specified canonical JSON encoding before treating hashes as interoperable signed commitments. +- The route deliberately does not issue rewards. Rewards require a separate authorization and policy path after verification. From 74e1acfca231068ae2149345b9ecb1c7abefd331 Mon Sep 17 00:00:00 2001 From: NeonTechnoX <89855167+NeonTechno@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:42:06 +0000 Subject: [PATCH 05/26] feat(portal): add MCP SDK dependency --- app-portal/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app-portal/package.json b/app-portal/package.json index ebce238..cd076aa 100644 --- a/app-portal/package.json +++ b/app-portal/package.json @@ -24,7 +24,8 @@ "tailwind-merge": "^2.2.1", "web3": "^4.6.0", "zustand": "^4.5.5", - "zod": "^3.23.8" + "zod": "^3.23.8", + "@modelcontextprotocol/sdk": "^1.12.1" }, "devDependencies": { "@types/node": "^20", From 624c899717e2f6fc7c21e3c9b795769aa29a297a Mon Sep 17 00:00:00 2001 From: NeonTechnoX <89855167+NeonTechno@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:43:06 +0000 Subject: [PATCH 06/26] fix(portal): use native Streamable HTTP MCP transport --- app-portal/src/lib/mcp/remote-client.ts | 125 ++++++++++++++++++------ 1 file changed, 95 insertions(+), 30 deletions(-) diff --git a/app-portal/src/lib/mcp/remote-client.ts b/app-portal/src/lib/mcp/remote-client.ts index b21bd1c..de4e8e0 100644 --- a/app-portal/src/lib/mcp/remote-client.ts +++ b/app-portal/src/lib/mcp/remote-client.ts @@ -1,12 +1,12 @@ -import { Client } from '@modelcontextprotocol/sdk/client/index.js' -import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' - -export interface MCPToolSummary { - name: string - description?: string - inputSchema: Record +type JsonRpcResponse = { + jsonrpc?: string + id?: number | string + result?: any + error?: { code?: number; message?: string; data?: unknown } } +const MCP_PROTOCOL_VERSION = '2025-03-26' + function getMcpConfig() { const endpoint = process.env.DRP_MCP_SERVER_URL if (!endpoint) throw new Error('DRP_MCP_SERVER_URL is not configured') @@ -17,36 +17,98 @@ function getMcpConfig() { return { endpoint, allowedTools, token: process.env.DRP_MCP_BEARER_TOKEN } } -export async function withRemoteMcp( - action: (client: Client, allowedTools: string[]) => Promise, +async function rpc( + endpoint: string, + token: string | undefined, + method: string, + params: Record | undefined, + sessionId?: string, + id = 1, +): Promise<{ response: JsonRpcResponse | null; sessionId?: string }> { + const headers: Record = { + Accept: 'application/json, text/event-stream', + 'Content-Type': 'application/json', + } + if (token) headers.Authorization = `Bearer ${token}` + if (sessionId) headers['Mcp-Session-Id'] = sessionId + + const response = await fetch(endpoint, { + method: 'POST', + headers, + body: JSON.stringify({ jsonrpc: '2.0', id, method, ...(params ? { params } : {}) }), + cache: 'no-store', + signal: AbortSignal.timeout(10_000), + }) + if (!response.ok) throw new Error(`MCP request ${method} failed with HTTP ${response.status}`) + + const nextSessionId = response.headers.get('mcp-session-id') ?? sessionId + const contentType = response.headers.get('content-type') ?? '' + if (response.status === 202 || response.status === 204) { + return { response: null, sessionId: nextSessionId ?? undefined } + } + + const text = await response.text() + let payload: JsonRpcResponse | null = null + if (contentType.includes('text/event-stream')) { + const data = text.split(/\r?\n/).filter((line) => line.startsWith('data:')).map((line) => line.slice(5).trim()) + for (const item of data) { + try { + const candidate = JSON.parse(item) as JsonRpcResponse + if (candidate.id === id || candidate.error) payload = candidate + } catch { + // Ignore non-JSON SSE keepalives and unrelated events. + } + } + } else if (text.trim()) { + payload = JSON.parse(text) as JsonRpcResponse + } + + if (payload?.error) throw new Error(payload.error.message ?? `MCP request ${method} failed`) + return { response: payload, sessionId: nextSessionId ?? undefined } +} + +async function withMcpSession( + action: (call: (method: string, params?: Record) => Promise) => Promise, ): Promise { const config = getMcpConfig() - const url = new URL(config.endpoint) - const headers: Record = {} - if (config.token) headers.Authorization = `Bearer ${config.token}` + let sessionId: string | undefined + let nextId = 1 - const transport = new StreamableHTTPClientTransport(url, { - requestInit: { headers }, + const initialized = await rpc(config.endpoint, config.token, 'initialize', { + protocolVersion: MCP_PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { name: 'drp-activity-verification', version: '0.1.0' }, + }, undefined, nextId++) + sessionId = initialized.sessionId + if (!initialized.response?.result) throw new Error('MCP server returned no initialize result') + + await rpc(config.endpoint, config.token, 'notifications/initialized', undefined, sessionId, nextId++) + const result = await action(async (method, params) => { + const reply = await rpc(config.endpoint, config.token, method, params, sessionId, nextId++) + sessionId = reply.sessionId + return reply.response }) - const client = new Client({ name: 'drp-activity-verification', version: '0.1.0' }) + return result +} - try { - await client.connect(transport) - return await action(client, config.allowedTools) - } finally { - await client.close().catch(() => undefined) - } +export interface MCPToolSummary { + name: string + description?: string + inputSchema: Record } export async function listAllowedMcpTools(): Promise { - return withRemoteMcp(async (client, allowedTools) => { - const result = await client.listTools() - return result.tools - .filter((tool) => allowedTools.includes(tool.name)) - .map((tool) => ({ + const config = getMcpConfig() + return withMcpSession(async (call) => { + const response = await call('tools/list') + const tools = response?.result?.tools + if (!Array.isArray(tools)) throw new Error('MCP server returned an invalid tools/list response') + return tools + .filter((tool: { name?: string }) => typeof tool.name === 'string' && config.allowedTools.includes(tool.name)) + .map((tool: { name: string; description?: string; inputSchema?: Record }) => ({ name: tool.name, description: tool.description, - inputSchema: tool.inputSchema as Record, + inputSchema: tool.inputSchema ?? {}, })) }) } @@ -55,8 +117,11 @@ export async function callAllowedMcpTool( name: string, args: Record, ): Promise { - return withRemoteMcp(async (client, allowedTools) => { - if (!allowedTools.includes(name)) throw new Error('MCP tool is not allowlisted') - return client.callTool({ name, arguments: args }) + const config = getMcpConfig() + if (!config.allowedTools.includes(name)) throw new Error('MCP tool is not allowlisted') + return withMcpSession(async (call) => { + const response = await call('tools/call', { name, arguments: args }) + if (!response?.result) throw new Error('MCP server returned no tool result') + return response.result }) } From d8e45328cbc695bdeebc3c89c55a29827ff746ee Mon Sep 17 00:00:00 2001 From: NeonTechnoX <89855167+NeonTechno@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:43:08 +0000 Subject: [PATCH 07/26] chore(portal): avoid unnecessary MCP SDK dependency --- app-portal/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app-portal/package.json b/app-portal/package.json index cd076aa..ebce238 100644 --- a/app-portal/package.json +++ b/app-portal/package.json @@ -24,8 +24,7 @@ "tailwind-merge": "^2.2.1", "web3": "^4.6.0", "zustand": "^4.5.5", - "zod": "^3.23.8", - "@modelcontextprotocol/sdk": "^1.12.1" + "zod": "^3.23.8" }, "devDependencies": { "@types/node": "^20", From 17642a7f2af5cf0f2ecabad7a0ef90462f0e420e Mon Sep 17 00:00:00 2001 From: NeonTechnoX <89855167+NeonTechno@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:43:11 +0000 Subject: [PATCH 08/26] fix(portal): handle undefined evidence hash input --- app-portal/src/lib/verification/activity-engine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-portal/src/lib/verification/activity-engine.ts b/app-portal/src/lib/verification/activity-engine.ts index 79217ea..8147436 100644 --- a/app-portal/src/lib/verification/activity-engine.ts +++ b/app-portal/src/lib/verification/activity-engine.ts @@ -47,7 +47,7 @@ function sha256(value: unknown): string { // JSON.stringify is sufficient for this initial commitment format only when // the caller supplies a stable object shape. A canonical JSON scheme is a // follow-up requirement before signatures are treated as portable proofs. - return createHash('sha256').update(JSON.stringify(value)).digest('hex') + const serialized = JSON.stringify(value)\n return createHash('sha256').update(serialized ?? 'null').digest('hex') } export function prepareActivityClaim(input: ActivityClaimInput): PreparedActivity { From 87159d880ff66b31639e224cb59e7832630e7f96 Mon Sep 17 00:00:00 2001 From: NeonTechnoX <89855167+NeonTechno@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:43:27 +0000 Subject: [PATCH 09/26] fix(portal): send MCP initialization as a notification --- app-portal/src/lib/mcp/remote-client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app-portal/src/lib/mcp/remote-client.ts b/app-portal/src/lib/mcp/remote-client.ts index de4e8e0..971247a 100644 --- a/app-portal/src/lib/mcp/remote-client.ts +++ b/app-portal/src/lib/mcp/remote-client.ts @@ -35,7 +35,7 @@ async function rpc( const response = await fetch(endpoint, { method: 'POST', headers, - body: JSON.stringify({ jsonrpc: '2.0', id, method, ...(params ? { params } : {}) }), + body: JSON.stringify({ jsonrpc: '2.0', ...(id === undefined ? {} : { id }), method, ...(params ? { params } : {}) }), cache: 'no-store', signal: AbortSignal.timeout(10_000), }) @@ -54,7 +54,7 @@ async function rpc( for (const item of data) { try { const candidate = JSON.parse(item) as JsonRpcResponse - if (candidate.id === id || candidate.error) payload = candidate + if ((id !== undefined && candidate.id === id) || candidate.error) payload = candidate } catch { // Ignore non-JSON SSE keepalives and unrelated events. } @@ -82,7 +82,7 @@ async function withMcpSession( sessionId = initialized.sessionId if (!initialized.response?.result) throw new Error('MCP server returned no initialize result') - await rpc(config.endpoint, config.token, 'notifications/initialized', undefined, sessionId, nextId++) + await rpc(config.endpoint, config.token, 'notifications/initialized', undefined, sessionId) const result = await action(async (method, params) => { const reply = await rpc(config.endpoint, config.token, method, params, sessionId, nextId++) sessionId = reply.sessionId From f892f72e78027cd149bb1825369f5c44237cf2f1 Mon Sep 17 00:00:00 2001 From: NeonTechnoX <89855167+NeonTechno@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:44:48 +0000 Subject: [PATCH 10/26] feat(portal): add activity verification submission page --- .../src/app/activity-verification/page.tsx | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 app-portal/src/app/activity-verification/page.tsx diff --git a/app-portal/src/app/activity-verification/page.tsx b/app-portal/src/app/activity-verification/page.tsx new file mode 100644 index 0000000..c8b27c3 --- /dev/null +++ b/app-portal/src/app/activity-verification/page.tsx @@ -0,0 +1,133 @@ +'use client' + +import { FormEvent, useState } from 'react' +import { useWallet } from '@/hooks/useWallet' + +type Result = { + activity?: { claimId: string; decision: string; reasons: string[]; evidence: Array<{ contentHash: string }> } + protocol?: { status: string; response?: unknown; message?: string; httpStatus?: number } + error?: string +} + +export default function ActivityVerificationPage() { + const { address, connect, isConnecting } = useWallet() + const [source, setSource] = useState<'github' | 'community'>('github') + const [activityType, setActivityType] = useState('merged_pull_request') + const [statement, setStatement] = useState('') + const [sourceId, setSourceId] = useState('') + const [sourceUrl, setSourceUrl] = useState('') + const [evidenceNotes, setEvidenceNotes] = useState('') + const [result, setResult] = useState(null) + const [busy, setBusy] = useState(false) + + async function submit(event: FormEvent) { + event.preventDefault() + setBusy(true) + setResult(null) + try { + const response = await fetch('/api/verification/evaluate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + subjectId: address, + source, + activityType, + statement, + humanReviewRequired: source === 'community', + evidence: [{ + sourceId, + evidenceType: source === 'github' ? 'github_reference' : 'community_activity_report', + sourceUrl: sourceUrl || undefined, + content: { notes: evidenceNotes, sourceId, sourceUrl: sourceUrl || null }, + }], + }), + }) + const payload = await response.json() as Result + setResult(payload) + } catch { + setResult({ error: 'Could not submit the claim. Check your connection and try again.' }) + } finally { + setBusy(false) + } + } + + return ( +
+
+

DRP • MCP Activity Verification

+

Verify activity with evidence, not assumptions.

+

Submit a GitHub contribution reference or a community activity report. DRP applies the same claim format, records evidence commitments, and can forward the request to the DRP Verification Network.

+
+ +
+ {[ + ['01', 'Connect a source', 'MCP tools are configured server-side and restricted by an explicit allowlist.'], + ['02', 'Prepare evidence', 'DRP hashes the supplied evidence payload and flags claims that need independent checks.'], + ['03', 'Verify and attest', 'Network acceptance is not a verified decision; verifier attestations and review still matter.'], + ].map(([number, title, text]) => ( +
+

{number}

+

{title}

+

{text}

+
+ ))} +
+ +
+
+

New activity claim

+ {address ? {address.slice(0, 8)}…{address.slice(-6)} : } +
+ +
+ Activity source +
+ + +
+
+ +
+ + +
+ +