diff --git a/apps/mcp-remote/.env.example b/apps/mcp-remote/.env.example index 23bd08d..850293b 100644 --- a/apps/mcp-remote/.env.example +++ b/apps/mcp-remote/.env.example @@ -1,3 +1,4 @@ DATABASE_URL=file:test.db DATABASE_TOKEN=needs_to_be_set_but_it_can_be_anything -VOYAGE_API_KEY=your_actual_api_key_here \ No newline at end of file +VOYAGE_API_KEY=your_actual_api_key_here +GITHUB_WEBHOOK_SECRET=some_secret \ No newline at end of file diff --git a/apps/mcp-remote/package.json b/apps/mcp-remote/package.json index 1164d4b..91dea67 100644 --- a/apps/mcp-remote/package.json +++ b/apps/mcp-remote/package.json @@ -65,6 +65,7 @@ "@sveltejs/mcp-schema": "workspace:^", "@sveltejs/mcp-server": "workspace:^", "@tmcp/transport-http": "^0.6.3", - "tmcp": "^1.14.0" + "tmcp": "^1.14.0", + "valibot": "^1.1.0" } } diff --git a/apps/mcp-remote/src/lib/schemas/index.ts b/apps/mcp-remote/src/lib/schemas/index.ts new file mode 100644 index 0000000..f9ddc3c --- /dev/null +++ b/apps/mcp-remote/src/lib/schemas/index.ts @@ -0,0 +1,32 @@ +import * as v from 'valibot'; + +// not the full schema but it contains the information we need +export const github_webhook_schema = v.object({ + action: v.union([v.literal('closed')]), + pull_request: v.object({ + patch_url: v.string(), + merged: v.boolean(), + user: v.object({ + login: v.string(), + }), + }), +}); + +export const github_content_schema = v.object({ + name: v.string(), + path: v.string(), + sha: v.string(), + size: v.number(), + url: v.string(), + html_url: v.string(), + git_url: v.string(), + download_url: v.nullable(v.string()), + type: v.literal('file'), + content: v.string(), + encoding: v.literal('base64'), + _links: v.object({ + self: v.string(), + git: v.string(), + html: v.string(), + }), +}); diff --git a/apps/mcp-remote/src/routes/webhooks/docs/+server.ts b/apps/mcp-remote/src/routes/webhooks/docs/+server.ts new file mode 100644 index 0000000..acaebf0 --- /dev/null +++ b/apps/mcp-remote/src/routes/webhooks/docs/+server.ts @@ -0,0 +1,54 @@ +import { github_content_schema, github_webhook_schema } from '$lib/schemas/index.js'; +import * as v from 'valibot'; + +export async function POST({ request, fetch }) { + const body = await request.json(); + // TODO add secret validation + const validated_pull_request = v.safeParse(github_webhook_schema, body); + if (!validated_pull_request.success) { + return new Response('Invalid payload', { status: 400 }); + } + + const { pull_request } = validated_pull_request.output; + + if (!pull_request.merged) { + return new Response(null, { status: 204 }); + } + + const patch = await fetch(pull_request.patch_url); + if (!patch.ok) { + return new Response('Failed to fetch patch', { status: 500 }); + } + + const patch_text = await patch.text(); + const files = [ + ...patch_text.matchAll( + /^diff --git\sa\/(?.+?)\sb\/\1\n(?:(?deleted|new)\sfile mode)?/gm, + ), + ].map((res) => ({ file: res.groups!.file, action: res.groups?.action ?? 'modified' })); + + for (const file of files) { + if (file.action === 'deleted') { + // delete path from db + continue; + } + const new_file_content = await fetch( + `https://api.github.com/repos/sveltejs/svelte.dev/contents/${file.file}`, + ); + if (!new_file_content.ok) { + // push file in queue and try again later? + continue; + } + const new_file_json = await new_file_content.json(); + const validated_content = v.safeParse(github_content_schema, new_file_json); + if (!validated_content.success) { + // push file in queue and try again later? + continue; + } + const content = Buffer.from(validated_content.output.content, 'base64').toString('utf-8'); + // save content and distilled content in the db + console.log({ content, file: file.file }); + } + + return new Response(null, { status: 204 }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a59e75..81e6e13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: tmcp: specifier: ^1.14.0 version: 1.14.0(typescript@5.9.2) + valibot: + specifier: ^1.1.0 + version: 1.1.0(typescript@5.9.2) devDependencies: '@eslint/compat': specifier: ^1.3.2