From 218a07663eac51e5de01249d14d83eb3be2ed430 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 16 Jun 2026 14:39:27 +0000 Subject: [PATCH 1/4] Add agent-readiness discovery endpoints and infrastructure Publish RFC 8288 Link headers, RFC 9727 API catalog, OAuth discovery metadata, MCP server card, agent skills index, auth.md, Content Signals in robots.txt, markdown negotiation, WebMCP tools, and DNS-AID zone templates for nextflow.io agent discovery. Co-authored-by: Edmund Miller --- Makefile | 2 + infra/cloudfront/README.md | 20 ++++ infra/cloudfront/markdown-negotiation.js | 18 ++++ infra/cloudfront/response-headers-policy.json | 23 +++++ infra/dns-aid/README.md | 18 ++++ infra/dns-aid/nextflow.io.zone | 12 +++ netlify.toml | 10 ++ netlify/edge-functions/markdown-negotiate.js | 36 +++++++ package.json | 2 + public/.well-known/agent-skills/index.json | 12 +++ .../agent-skills/nextflow-docs/SKILL.md | 26 +++++ public/.well-known/api-catalog | 25 +++++ public/.well-known/jwks.json | 3 + public/.well-known/mcp/server-card.json | 11 +++ public/.well-known/oauth-authorization-server | 20 ++++ public/.well-known/oauth-protected-resource | 7 ++ public/_headers | 50 ++++++++++ public/auth.md | 64 +++++++++++++ public/index.md | 58 +++++++++++ public/openapi.json | 51 ++++++++++ public/robots.txt | 1 + publish.sh | 4 +- scripts/generate-agent-skills-index.mjs | 52 ++++++++++ scripts/set-s3-agent-metadata.sh | 47 +++++++++ src/components/WebMcpBridge/index.tsx | 95 +++++++++++++++++++ src/layouts/Layout.astro | 7 ++ 26 files changed, 673 insertions(+), 1 deletion(-) create mode 100644 infra/cloudfront/README.md create mode 100644 infra/cloudfront/markdown-negotiation.js create mode 100644 infra/cloudfront/response-headers-policy.json create mode 100644 infra/dns-aid/README.md create mode 100644 infra/dns-aid/nextflow.io.zone create mode 100644 netlify/edge-functions/markdown-negotiate.js create mode 100644 public/.well-known/agent-skills/index.json create mode 100644 public/.well-known/agent-skills/nextflow-docs/SKILL.md create mode 100644 public/.well-known/api-catalog create mode 100644 public/.well-known/jwks.json create mode 100644 public/.well-known/mcp/server-card.json create mode 100644 public/.well-known/oauth-authorization-server create mode 100644 public/.well-known/oauth-protected-resource create mode 100644 public/_headers create mode 100644 public/auth.md create mode 100644 public/index.md create mode 100644 public/openapi.json create mode 100644 scripts/generate-agent-skills-index.mjs create mode 100755 scripts/set-s3-agent-metadata.sh create mode 100644 src/components/WebMcpBridge/index.tsx diff --git a/Makefile b/Makefile index 57f24c2ca..f2cf82caa 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,8 @@ publish: --acl public-read \ --exclude "$0" + bash scripts/set-s3-agent-metadata.sh + invalidate: aws cloudfront create-invalidation --distribution-id E3RPV5P71OW0UF --paths '/*' diff --git a/infra/cloudfront/README.md b/infra/cloudfront/README.md new file mode 100644 index 000000000..68ba7378c --- /dev/null +++ b/infra/cloudfront/README.md @@ -0,0 +1,20 @@ +# CloudFront agent-readiness configuration + +Production hosting uses S3 + CloudFront. Apply these after deploying static files. + +## Link response headers (RFC 8288) + +1. Create a response headers policy from `response-headers-policy.json`, or attach equivalent `Link` headers to the default cache behavior for `/` and `/index.html`. +2. Alternatively, rely on `scripts/set-s3-agent-metadata.sh` (runs during `make publish`) which sets S3 object metadata on `index.html`. + +## Markdown content negotiation + +1. Publish the CloudFront Function in `markdown-negotiation.js`. +2. Associate it with **viewer-request** on the default behavior. +3. Ensure `index.md` is deployed to S3 (copied from `public/index.md` via the Astro build). + +The function rewrites `/` and `/index.html` to `/index.md` when `Accept: text/markdown` is present. + +## Netlify previews + +PR previews use `public/_headers` for Link headers and `netlify/edge-functions/markdown-negotiate.js` for markdown negotiation. diff --git a/infra/cloudfront/markdown-negotiation.js b/infra/cloudfront/markdown-negotiation.js new file mode 100644 index 000000000..e4ec4c7ed --- /dev/null +++ b/infra/cloudfront/markdown-negotiation.js @@ -0,0 +1,18 @@ +// CloudFront Function: serve markdown when Accept: text/markdown is present. +// Associate with viewer-request on the nextflow.io CloudFront distribution. +function handler(event) { + var request = event.request; + var headers = request.headers; + var accept = headers.accept ? headers.accept.value : ""; + var uri = request.uri; + + if (accept.indexOf("text/markdown") === -1) { + return request; + } + + if (uri === "/" || uri === "/index.html") { + request.uri = "/index.md"; + } + + return request; +} diff --git a/infra/cloudfront/response-headers-policy.json b/infra/cloudfront/response-headers-policy.json new file mode 100644 index 000000000..71d276381 --- /dev/null +++ b/infra/cloudfront/response-headers-policy.json @@ -0,0 +1,23 @@ +{ + "Comment": "Link response headers for nextflow.io agent discovery (RFC 8288)", + "Name": "nextflow-io-agent-discovery-headers", + "HeadersConfig": { + "HeaderBehavior": "whitelist", + "Headers": { + "Quantity": 1, + "Items": ["Link"] + } + }, + "CustomHeadersConfig": { + "Quantity": 1, + "Items": [ + { + "Header": "Link", + "Value": "; rel=\"api-catalog\", ; rel=\"service-doc\", ; rel=\"service-desc\"", + "Override": false + } + ] + }, + "SecurityHeadersConfig": {}, + "CorsConfig": {} +} diff --git a/infra/dns-aid/README.md b/infra/dns-aid/README.md new file mode 100644 index 000000000..6d49b6950 --- /dev/null +++ b/infra/dns-aid/README.md @@ -0,0 +1,18 @@ +# DNS for AI Discovery (DNS-AID) + +Publish the records in `nextflow.io.zone` under the `_agents` namespace for `nextflow.io`. + +## Requirements + +1. Add `_index._agents.nextflow.io` and `_a2a._agents.nextflow.io` SVCB records (see zone file). +2. Enable DNSSEC on the public zone and publish DS records at your domain registrar. +3. Verify with DNS-over-HTTPS: + +```bash +curl -s 'https://cloudflare-dns.com/dns-query?name=_index._agents.nextflow.io&type=SVCB' \ + -H 'accept: application/dns-json' +``` + +## Production note + +DNS-AID records are managed outside this repository (Route 53, Cloudflare DNS, or your registrar). Apply the zone file through your DNS operations workflow after review. diff --git a/infra/dns-aid/nextflow.io.zone b/infra/dns-aid/nextflow.io.zone new file mode 100644 index 000000000..75880db93 --- /dev/null +++ b/infra/dns-aid/nextflow.io.zone @@ -0,0 +1,12 @@ +; DNS for AI Discovery (DNS-AID) records for nextflow.io +; Deploy this zone (or merge these records) with your DNS provider. +; Sign the zone with DNSSEC and publish DS records at the registrar. + +$ORIGIN nextflow.io. +$TTL 3600 + +; Index entrypoint — points agents to the site's API catalog +_index._agents.nextflow.io. IN SVCB 1 nextflow.io. alpn="h3,h2" port=443 mandatory=alpn,port + +; Agent-to-agent discovery entrypoint +_a2a._agents.nextflow.io. IN SVCB 1 nextflow.io. alpn="h3,h2" port=443 mandatory=alpn,port diff --git a/netlify.toml b/netlify.toml index 1079e0202..78164d693 100644 --- a/netlify.toml +++ b/netlify.toml @@ -5,6 +5,16 @@ [build.environment] NODE_VERSION = "20.11.0" +[[edge_functions]] + function = "markdown-negotiate" + path = "/" + cache = "manual" + +[[edge_functions]] + function = "markdown-negotiate" + path = "/index.html" + cache = "manual" + [[redirects]] from = "/vscode" to = "https://marketplace.visualstudio.com/items?itemName=nextflow.nextflow" diff --git a/netlify/edge-functions/markdown-negotiate.js b/netlify/edge-functions/markdown-negotiate.js new file mode 100644 index 000000000..7dcee7038 --- /dev/null +++ b/netlify/edge-functions/markdown-negotiate.js @@ -0,0 +1,36 @@ +const MARKDOWN_PATHS = { + "/": "/index.md", + "/index.html": "/index.md", +}; + +export default async function handler(request, context) { + const accept = request.headers.get("Accept") ?? ""; + if (!accept.includes("text/markdown")) { + return context.next(); + } + + const url = new URL(request.url); + const markdownPath = MARKDOWN_PATHS[url.pathname]; + if (!markdownPath) { + return context.next(); + } + + const markdownUrl = new URL(markdownPath, url.origin); + const markdownResponse = await context.rewrite(markdownUrl.toString()); + if (!markdownResponse.ok) { + return context.next(); + } + + const body = await markdownResponse.text(); + const tokenEstimate = Math.ceil(body.length / 4); + + return new Response(body, { + status: 200, + headers: { + "Content-Type": "text/markdown; charset=utf-8", + "x-markdown-tokens": String(tokenEstimate), + Vary: "Accept", + "Cache-Control": "public, max-age=300, must-revalidate", + }, + }); +} diff --git a/package.json b/package.json index dd21a83a0..42adf7197 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "dev": "astro dev", "start": "astro dev", "docs": "bash build_docs.sh", + "prebuild": "node scripts/generate-agent-skills-index.mjs", "build": "astro check && astro build && bash build_docs.sh", + "generate:agent-skills-index": "node scripts/generate-agent-skills-index.mjs", "preview": "astro preview", "astro": "astro" }, diff --git a/public/.well-known/agent-skills/index.json b/public/.well-known/agent-skills/index.json new file mode 100644 index 000000000..dc6bde2f0 --- /dev/null +++ b/public/.well-known/agent-skills/index.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json", + "skills": [ + { + "name": "nextflow-docs", + "type": "skill-md", + "description": "Navigate Nextflow documentation, examples, and training resources.", + "url": "https://nextflow.io/.well-known/agent-skills/nextflow-docs/SKILL.md", + "digest": "sha256:1545501e225e877dc20271e11db6078a3d15116db9259a70b219561f4d367bfb" + } + ] +} diff --git a/public/.well-known/agent-skills/nextflow-docs/SKILL.md b/public/.well-known/agent-skills/nextflow-docs/SKILL.md new file mode 100644 index 000000000..8d8d5bfc2 --- /dev/null +++ b/public/.well-known/agent-skills/nextflow-docs/SKILL.md @@ -0,0 +1,26 @@ +--- +name: nextflow-docs +description: Navigate Nextflow documentation, examples, and training resources from nextflow.io. +--- + +# Nextflow Documentation Navigation + +Use this skill when helping users find Nextflow documentation, examples, or training material. + +## Key URLs + +- Official docs: https://docs.seqera.io/nextflow/ +- Training portal: https://training.nextflow.io/latest/ +- Basic pipeline example: https://nextflow.io/basic-pipeline.html +- Community forum: https://community.seqera.io/tag/nextflow +- GitHub repository: https://github.com/nextflow-io/nextflow + +## Discovery endpoints + +- API catalog: https://nextflow.io/.well-known/api-catalog +- OpenAPI spec: https://nextflow.io/openapi.json +- RSS feed: https://nextflow.io/feed.xml + +## Markdown access + +Request pages with `Accept: text/markdown` or read `https://nextflow.io/index.md` for the homepage in markdown. diff --git a/public/.well-known/api-catalog b/public/.well-known/api-catalog new file mode 100644 index 000000000..090116f3f --- /dev/null +++ b/public/.well-known/api-catalog @@ -0,0 +1,25 @@ +{ + "linkset": [ + { + "anchor": "https://nextflow.io/", + "service-desc": [ + { + "href": "https://nextflow.io/openapi.json", + "type": "application/json" + } + ], + "service-doc": [ + { + "href": "https://docs.seqera.io/nextflow/", + "type": "text/html" + } + ], + "status": [ + { + "href": "https://nextflow.io/feed.xml", + "type": "application/rss+xml" + } + ] + } + ] +} diff --git a/public/.well-known/jwks.json b/public/.well-known/jwks.json new file mode 100644 index 000000000..faf87ec30 --- /dev/null +++ b/public/.well-known/jwks.json @@ -0,0 +1,3 @@ +{ + "keys": [] +} diff --git a/public/.well-known/mcp/server-card.json b/public/.well-known/mcp/server-card.json new file mode 100644 index 000000000..b900964e3 --- /dev/null +++ b/public/.well-known/mcp/server-card.json @@ -0,0 +1,11 @@ +{ + "serverInfo": { + "name": "nextflow.io", + "version": "1.0.0" + }, + "transport": { + "type": "streamable-http", + "endpoint": "https://nextflow.io/mcp" + }, + "capabilities": ["tools", "resources"] +} diff --git a/public/.well-known/oauth-authorization-server b/public/.well-known/oauth-authorization-server new file mode 100644 index 000000000..1bee1b145 --- /dev/null +++ b/public/.well-known/oauth-authorization-server @@ -0,0 +1,20 @@ +{ + "_comment": "nextflow.io is a public marketing and documentation site with no protected APIs today. OAuth metadata is published for agent discovery per RFC 8414. Endpoints are reserved and do not accept live OAuth flows.", + "issuer": "https://nextflow.io", + "authorization_endpoint": "https://nextflow.io/oauth/authorize", + "token_endpoint": "https://nextflow.io/oauth/token", + "jwks_uri": "https://nextflow.io/.well-known/jwks.json", + "grant_types_supported": ["client_credentials"], + "response_types_supported": ["code"], + "scopes_supported": [], + "token_endpoint_auth_methods_supported": ["none"], + "agent_auth": { + "skill": "https://nextflow.io/auth.md", + "register_uri": "https://nextflow.io/agent/register", + "identity_types_supported": ["anonymous"], + "anonymous": { + "credential_types_supported": ["access_token"] + }, + "claim_uri": "https://nextflow.io/agent/claim" + } +} diff --git a/public/.well-known/oauth-protected-resource b/public/.well-known/oauth-protected-resource new file mode 100644 index 000000000..649450ac9 --- /dev/null +++ b/public/.well-known/oauth-protected-resource @@ -0,0 +1,7 @@ +{ + "resource": "https://nextflow.io", + "authorization_servers": ["https://nextflow.io"], + "scopes_supported": [], + "bearer_methods_supported": ["header"], + "resource_documentation": "https://docs.seqera.io/nextflow/" +} diff --git a/public/_headers b/public/_headers new file mode 100644 index 000000000..08f6fe11d --- /dev/null +++ b/public/_headers @@ -0,0 +1,50 @@ +/ + Link: ; rel="api-catalog", ; rel="service-doc", ; rel="service-desc" + +/index.html + Link: ; rel="api-catalog", ; rel="service-doc", ; rel="service-desc" + +/.well-known/api-catalog + Content-Type: application/linkset+json + Cache-Control: public, max-age=300, must-revalidate + Access-Control-Allow-Origin: * + +/.well-known/oauth-authorization-server + Content-Type: application/json + Cache-Control: public, max-age=300, must-revalidate + Access-Control-Allow-Origin: * + +/.well-known/oauth-protected-resource + Content-Type: application/json + Cache-Control: public, max-age=300, must-revalidate + Access-Control-Allow-Origin: * + +/.well-known/mcp/server-card.json + Content-Type: application/json + Cache-Control: public, max-age=300, must-revalidate + Access-Control-Allow-Origin: * + +/.well-known/agent-skills/index.json + Content-Type: application/json + Cache-Control: public, max-age=300, must-revalidate + Access-Control-Allow-Origin: * + +/.well-known/jwks.json + Content-Type: application/json + Cache-Control: public, max-age=300, must-revalidate + Access-Control-Allow-Origin: * + +/auth.md + Content-Type: text/markdown; charset=utf-8 + Cache-Control: public, max-age=300, must-revalidate + Access-Control-Allow-Origin: * + +/index.md + Content-Type: text/markdown; charset=utf-8 + Cache-Control: public, max-age=300, must-revalidate + Access-Control-Allow-Origin: * + +/openapi.json + Content-Type: application/json + Cache-Control: public, max-age=300, must-revalidate + Access-Control-Allow-Origin: * diff --git a/public/auth.md b/public/auth.md new file mode 100644 index 000000000..a958e5ec5 --- /dev/null +++ b/public/auth.md @@ -0,0 +1,64 @@ +# auth.md + +You are an agent. This document describes how to access programmatic resources on **nextflow.io**, the official website for the Nextflow workflow framework. + +Not an agent? See the [Nextflow documentation](https://docs.seqera.io/nextflow/) for human-oriented guides. + +## Public access (no registration required) + +Most resources on nextflow.io are public and do not require authentication: + +- **Documentation** — [https://docs.seqera.io/nextflow/](https://docs.seqera.io/nextflow/) +- **RSS feed** — [https://nextflow.io/feed.xml](https://nextflow.io/feed.xml) +- **API catalog** — [https://nextflow.io/.well-known/api-catalog](https://nextflow.io/.well-known/api-catalog) +- **OpenAPI spec** — [https://nextflow.io/openapi.json](https://nextflow.io/openapi.json) +- **Markdown pages** — send `Accept: text/markdown` to any page URL, or read `https://nextflow.io/index.md` + +## Discovery + +Start with these machine-readable discovery documents: + +1. `GET /.well-known/oauth-protected-resource` — protected resource metadata (RFC 9728) +2. `GET /.well-known/oauth-authorization-server` — authorization server metadata (RFC 8414) including the `agent_auth` block +3. `GET /.well-known/api-catalog` — API catalog (RFC 9727) +4. `GET /.well-known/mcp/server-card.json` — MCP server card +5. `GET /.well-known/agent-skills/index.json` — agent skills index + +## Agent registration + +nextflow.io does not operate protected APIs that require agent credentials today. The registration endpoints below are reserved for future programmatic access: + +| Endpoint | Method | Purpose | +| --- | --- | --- | +| `https://nextflow.io/agent/register` | `POST` | Reserved agent registration endpoint | +| `https://nextflow.io/agent/claim` | `POST` | Reserved claim ceremony endpoint | + +**Supported identity type:** `anonymous` (read-only public resources only). + +**Credential types:** `access_token` (not currently issued). + +If you receive `404` or `501` from a registration endpoint, treat all public resources as unauthenticated read-only access. + +## Using credentials + +When credentials become available, send them on API requests: + +```http +GET /feed.xml HTTP/1.1 +Host: nextflow.io +Authorization: Bearer +``` + +## Errors + +| Status | Meaning | Action | +| --- | --- | --- | +| 401 | Authentication required but not provided | Re-read `/.well-known/oauth-protected-resource` | +| 404 | Endpoint not implemented | Use public read-only resources instead | +| 429 | Rate limited | Exponential backoff, then retry | + +## Related standards + +- [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414) — OAuth Authorization Server Metadata +- [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) — OAuth Protected Resource Metadata +- [auth.md protocol](https://github.com/workos/auth.md) diff --git a/public/index.md b/public/index.md new file mode 100644 index 000000000..60b549683 --- /dev/null +++ b/public/index.md @@ -0,0 +1,58 @@ +# Nextflow + +> Reproducible Scientific Workflows at Scale + +Nextflow enables scalable, reproducible, and portable scientific workflows for research and production use cases. + +## Quick links + +- [Documentation](https://docs.seqera.io/nextflow/) +- [Community forum](https://community.seqera.io/tag/nextflow) +- [Training](https://training.nextflow.io/latest/) +- [Examples](/basic-pipeline.html) +- [VS Code extension](https://marketplace.visualstudio.com/items?itemName=nextflow.nextflow) +- [Seqera AI assistant](https://seqera.io/ask-ai/) + +## Features + +### Fast prototyping + +Nextflow allows you to write a computational pipeline by making it simpler to put together many different tasks. You may reuse your existing scripts and tools without learning a new language. + +### Reproducibility + +Nextflow supports [Docker](http://docker.io) and [Singularity](http://singularity.lbl.gov/) containers, with Git integration for versioned, self-contained pipelines. + +### Continuous checkpoints + +Intermediate results are automatically tracked so you can resume from the last successful step. + +### Portable + +Pipelines run on GridEngine, SLURM, LSF, PBS, Kubernetes, AWS, Google Cloud, Azure, and more without code changes. + +### Stream oriented + +Nextflow extends the Unix pipes model with a fluent DSL for complex stream interactions and functional composition. + +### Unified parallelism + +Parallelisation is defined by process input and output declarations; applications scale transparently. + +## Community resources + +- [nf-core](https://nf-co.re/) — curated community pipelines +- [Seqera Pipelines](https://seqera.io/pipelines/) — browse open-source pipelines +- [GitHub issues](https://github.com/nextflow-io/nextflow/issues) — report bugs or request features + +## Discovery + +Machine-readable resources for agents: + +- API catalog: `/.well-known/api-catalog` +- OpenAPI: `/openapi.json` +- MCP server card: `/.well-known/mcp/server-card.json` +- Agent skills: `/.well-known/agent-skills/index.json` +- Auth: `/auth.md` + +Supported by [Seqera](https://seqera.io). diff --git a/public/openapi.json b/public/openapi.json new file mode 100644 index 000000000..33083da0c --- /dev/null +++ b/public/openapi.json @@ -0,0 +1,51 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Nextflow.io Public Endpoints", + "version": "1.0.0", + "description": "Read-only public endpoints exposed by the Nextflow marketing site." + }, + "servers": [ + { + "url": "https://nextflow.io" + } + ], + "paths": { + "/feed.xml": { + "get": { + "summary": "RSS feed", + "description": "Atom/RSS feed of Nextflow blog posts and updates.", + "responses": { + "200": { + "description": "RSS feed document", + "content": { + "application/rss+xml": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/.well-known/api-catalog": { + "get": { + "summary": "API catalog", + "description": "RFC 9727 linkset describing public programmatic resources.", + "responses": { + "200": { + "description": "API catalog linkset", + "content": { + "application/linkset+json": { + "schema": { + "type": "object" + } + } + } + } + } + } + } + } +} diff --git a/public/robots.txt b/public/robots.txt index 34b8bde68..40e487932 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -12,5 +12,6 @@ Disallow: /misc/ Disallow: /logs/ Disallow: /tests/ Disallow: /releases/ +Content-Signal: ai-train=no, search=yes, ai-input=yes Sitemap: https://nextflow.io/sitemap-index.xml diff --git a/publish.sh b/publish.sh index deb025dcb..de586f381 100755 --- a/publish.sh +++ b/publish.sh @@ -17,4 +17,6 @@ aws s3 sync --delete output/docs s3://www2.nextflow.io/docs \ --storage-class STANDARD \ --acl public-read \ --exclude "$0" \ - "$@" \ No newline at end of file + "$@" + +bash "$(dirname "$0")/scripts/set-s3-agent-metadata.sh" \ No newline at end of file diff --git a/scripts/generate-agent-skills-index.mjs b/scripts/generate-agent-skills-index.mjs new file mode 100644 index 000000000..8090078a0 --- /dev/null +++ b/scripts/generate-agent-skills-index.mjs @@ -0,0 +1,52 @@ +#!/usr/bin/env node +/** + * Generates /.well-known/agent-skills/index.json with SHA-256 digests + * computed from the bytes served at each skill URL. + */ +import { createHash } from "node:crypto"; +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const publicDir = join(__dirname, "..", "public"); +const siteOrigin = "https://nextflow.io"; + +const localSkills = [ + { + name: "nextflow-docs", + type: "skill-md", + description: "Navigate Nextflow documentation, examples, and training resources.", + path: ".well-known/agent-skills/nextflow-docs/SKILL.md", + }, +]; + +function sha256OfBytes(bytes) { + const hex = createHash("sha256").update(bytes).digest("hex"); + return `sha256:${hex}`; +} + +function sha256OfFile(filePath) { + const bytes = readFileSync(filePath); + return sha256OfBytes(bytes); +} + +const skills = localSkills.map((skill) => { + const filePath = join(publicDir, skill.path); + return { + name: skill.name, + type: skill.type, + description: skill.description, + url: `${siteOrigin}/${skill.path}`, + digest: sha256OfFile(filePath), + }; +}); + +const index = { + $schema: "https://schemas.agentskills.io/discovery/0.2.0/schema.json", + skills, +}; + +const outputPath = join(publicDir, ".well-known", "agent-skills", "index.json"); +writeFileSync(outputPath, `${JSON.stringify(index, null, 2)}\n`); +console.log(`Wrote ${outputPath} with ${skills.length} skill(s).`); diff --git a/scripts/set-s3-agent-metadata.sh b/scripts/set-s3-agent-metadata.sh new file mode 100755 index 000000000..cdc9d3279 --- /dev/null +++ b/scripts/set-s3-agent-metadata.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Set Content-Type metadata on agent-discovery objects after S3 sync. +# Invoked by publish.sh after the main sync completes. + +set -euo pipefail + +BUCKET="${NXF_S3_BUCKET:-www2.nextflow.io}" + +declare -A CONTENT_TYPES=( + [".well-known/api-catalog"]="application/linkset+json" + [".well-known/oauth-authorization-server"]="application/json" + [".well-known/oauth-protected-resource"]="application/json" + [".well-known/jwks.json"]="application/json" + [".well-known/mcp/server-card.json"]="application/json" + [".well-known/agent-skills/index.json"]="application/json" + [".well-known/agent-skills/nextflow-docs/SKILL.md"]="text/markdown; charset=utf-8" + ["auth.md"]="text/markdown; charset=utf-8" + ["index.md"]="text/markdown; charset=utf-8" + ["openapi.json"]="application/json" +) + +for key in "${!CONTENT_TYPES[@]}"; do + content_type="${CONTENT_TYPES[$key]}" + if aws s3api head-object --bucket "$BUCKET" --key "$key" >/dev/null 2>&1; then + aws s3 cp "s3://${BUCKET}/${key}" "s3://${BUCKET}/${key}" \ + --metadata-directive REPLACE \ + --content-type "$content_type" \ + --acl public-read + echo "Set Content-Type for ${key} -> ${content_type}" + else + echo "Skipping ${key} (not found in bucket)" + fi +done + +# Link header on homepage objects +LINK_HEADER='; rel="api-catalog", ; rel="service-doc", ; rel="service-desc"' + +for key in index.html; do + if aws s3api head-object --bucket "$BUCKET" --key "$key" >/dev/null 2>&1; then + aws s3 cp "s3://${BUCKET}/${key}" "s3://${BUCKET}/${key}" \ + --metadata-directive REPLACE \ + --content-type "text/html; charset=utf-8" \ + --metadata "Link=${LINK_HEADER}" \ + --acl public-read + echo "Set Link metadata for ${key}" + fi +done diff --git a/src/components/WebMcpBridge/index.tsx b/src/components/WebMcpBridge/index.tsx new file mode 100644 index 000000000..7c9761cf6 --- /dev/null +++ b/src/components/WebMcpBridge/index.tsx @@ -0,0 +1,95 @@ +import { useEffect } from "react"; + +type WebMcpTool = { + name: string; + description: string; + inputSchema: Record; + execute: (input: Record) => Promise; +}; + +const tools: WebMcpTool[] = [ + { + name: "get_documentation_links", + description: "Return canonical Nextflow documentation and training URLs.", + inputSchema: { + type: "object", + properties: {}, + }, + execute: async () => ({ + documentation: "https://docs.seqera.io/nextflow/", + training: "https://training.nextflow.io/latest/", + community: "https://community.seqera.io/tag/nextflow", + examples: "https://nextflow.io/examples.html", + api_catalog: "https://nextflow.io/.well-known/api-catalog", + }), + }, + { + name: "get_examples", + description: "List Nextflow pipeline example pages available on nextflow.io.", + inputSchema: { + type: "object", + properties: {}, + }, + execute: async () => [ + { title: "Basic pipeline", url: "https://nextflow.io/basic-pipeline.html" }, + { title: "Mixing scripting languages", url: "https://nextflow.io/mixing-scripting-languages.html" }, + { title: "BLAST pipeline", url: "https://nextflow.io/blast-pipeline.html" }, + { title: "RNA-seq pipeline", url: "https://nextflow.io/rna-seq-pipeline.html" }, + { title: "Machine learning pipeline", url: "https://nextflow.io/machine-learning-pipeline.html" }, + ], + }, + { + name: "search_site", + description: "Search nextflow.io pages by keyword against known site resources.", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "Search keyword" }, + }, + required: ["query"], + }, + execute: async ({ query }) => { + const q = String(query ?? "").toLowerCase(); + const pages = [ + { title: "Home", url: "https://nextflow.io/", keywords: ["nextflow", "workflow", "pipeline", "reproducible"] }, + { title: "Examples", url: "https://nextflow.io/examples.html", keywords: ["example", "tutorial", "pipeline"] }, + { title: "About", url: "https://nextflow.io/about-us.html", keywords: ["about", "seqera", "team"] }, + { title: "Basic pipeline", url: "https://nextflow.io/basic-pipeline.html", keywords: ["basic", "hello", "world"] }, + { title: "Documentation", url: "https://docs.seqera.io/nextflow/", keywords: ["docs", "documentation", "reference", "manual"] }, + { title: "Training", url: "https://training.nextflow.io/latest/", keywords: ["training", "course", "learn"] }, + ]; + return pages.filter( + (page) => + page.title.toLowerCase().includes(q) || + page.keywords.some((keyword) => keyword.includes(q) || q.includes(keyword)), + ); + }, + }, +]; + +export default function WebMcpBridge() { + useEffect(() => { + const modelContext = (navigator as Navigator & { modelContext?: Record }).modelContext; + if (!modelContext) { + return; + } + + const abortController = new AbortController(); + + if (typeof modelContext.provideContext === "function") { + (modelContext.provideContext as (ctx: { tools: WebMcpTool[] }) => void)({ tools }); + } else if (typeof modelContext.registerTool === "function") { + for (const tool of tools) { + (modelContext.registerTool as (tool: WebMcpTool, opts: { signal: AbortSignal }) => void)(tool, { + signal: abortController.signal, + }); + } + } + + return () => { + abortController.abort(); + }; + }, []); + + return null; +} diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 21314bf33..bedf9e8d1 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -4,6 +4,7 @@ import "../styles/focus-styles.css"; import MenuComponent from "@components/Menu/Menu"; import Footer from "@components/Footer.astro"; import CookieBanner from "@components/CookieBanner/index.tsx"; +import WebMcpBridge from "@components/WebMcpBridge/index.tsx"; import { existsSync } from "fs"; import path from "path"; @@ -47,6 +48,11 @@ if (!existsSync(path.join(path.resolve("./public"), image_path))) { + + + + + @@ -122,6 +128,7 @@ if (!existsSync(path.join(path.resolve("./public"), image_path))) { +