diff --git a/submissions/mcp-hackathon/kele0929-venueclock/RIGHTS.md b/submissions/mcp-hackathon/kele0929-venueclock/RIGHTS.md new file mode 100644 index 0000000..058bb4e --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/RIGHTS.md @@ -0,0 +1,16 @@ +# Submission rights declaration + +Project: `VenueClock` +Submission slug: `kele0929-venueclock` +Submitter: `kele0929` +Date: `2026-09-04` + +The submitter confirms that they own, or have sufficient authorization for, the source code, dependencies, service, data, branding, and other materials submitted in this pull request. + +Subject to the official program terms, the submitter authorizes X-Agent to retain, reproduce, audit, test, archive, and publish the submitted program artifact for judging, fraud prevention, dispute handling, ecosystem submission, and post-award accountability. Closing the pull request, deleting a fork, or deleting an external repository does not revoke the official archive rights attached to an accepted and rewarded entry. + +Third-party components and their licenses: none. Runtime has no npm dependencies. Holiday dates were transcribed from public exchange/government calendars (Nasdaq Trader 2026 holiday schedule; NYSE Group holiday schedule; GOV.UK bank holidays) as facts, not as copied software. + +Exceptions or restrictions: none + +This template is an operational declaration, not a substitute for event terms reviewed by qualified counsel. diff --git a/submissions/mcp-hackathon/kele0929-venueclock/SUBMISSION.md b/submissions/mcp-hackathon/kele0929-venueclock/SUBMISSION.md new file mode 100644 index 0000000..924bcac --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/SUBMISSION.md @@ -0,0 +1,59 @@ +# VenueClock + +## Capability + +- **One-line description:** Return the market session state (pre-market, regular, after-hours, holiday, weekend, or closed) and next open/close for NYSE, Nasdaq, LSE, and 24/7 crypto. +- **Who it helps:** AI agents and tools that must not hallucinate trading hours, DST, or 2026 exchange holidays when scheduling or explaining market availability. +- **Capability boundary:** Session calendars only. No orders, prices, wallet scoring, exploit detection, or financial advice. Named holidays are encoded for 2026. + +## Live API + +- **API base URL:** https://kele0929.github.io/v1 +- **Health-check URL:** https://kele0929.github.io/health.json +- **Authentication:** none +- **Rate limits / known limits:** Public GitHub Pages. Keep request volume modest. JSON bodies are small. Unknown static paths return GitHub Pages HTML 404; the Node server in `source/` returns JSON 404/400 instead. +- **API contract:** `source/openapi.json` and `source/README.md` + +## Source and reproducibility + +- **Source repository:** https://github.com/kele0929/venueclock +- **Review commit:** `d1b84e00cb87b7e8811a020f497855b67bc00303` +- **Source submitted in this PR:** `source/` +- **Run tests:** `npm test` +- **Run locally:** `REVIEW_COMMIT=$(git rev-parse HEAD) npm start` then `curl http://127.0.0.1:8787/health.json` +- **Deploy:** `REVIEW_COMMIT= npm run generate -- --out ./site` and publish `site/` to the GitHub Pages user site `kele0929/kele0929.github.io` on `main` +- **Version binding:** `GET /health.json` and `GET /.well-known/xagent-verification.json` both report commit `d1b84e00cb87b7e8811a020f497855b67bc00303` + +The API exposes: + +```json +// GET https://kele0929.github.io/health.json +{"status":"ok","commit":"d1b84e00cb87b7e8811a020f497855b67bc00303"} +``` + +```json +// GET https://kele0929.github.io/.well-known/xagent-verification.json +{"schemaVersion":1,"slug":"kele0929-venueclock","commit":"d1b84e00cb87b7e8811a020f497855b67bc00303"} +``` + +## Verification + +The reproducible call instructions and redacted example responses are in `verification/README.md`. + +- **Health-check result:** HTTP 200 JSON `status=ok` with the review commit +- **Capability call:** `GET /v1/resolve/xnys/2026-09-04.json` returns Friday sessions including regular 09:30–16:00 America/New_York +- **Expected error behavior:** `GET /v1/resolve/xnys/2026-09-07.json` is Labor Day (`status=holiday`, empty sessions). Invalid dates / unknown venues on the Node server return JSON 400/404. + +## Security and data handling + +- **Data collected:** none +- **Purpose and retention:** none +- **Third parties / outbound network calls:** none at runtime (static JSON). Holiday tables were compiled from public Nasdaq/NYSE and GOV.UK calendars at build time. +- **Secrets:** No secrets are committed. Review access is supplied only through an approved private channel when required. +- **Known risks / restrictions:** Calendars can change if an exchange publishes an unscheduled close. This snapshot is the 2026 schedule encoded in source. Not trading advice. + +## Support + +- **Team / builder:** kele0929 +- **Contact:** GitHub @kele0929 · 1253719405@qq.com +- **License / rights:** MIT. The submitter authored this source and authorizes review, archival, and deployment under the program terms. diff --git a/submissions/mcp-hackathon/kele0929-venueclock/source/.github/workflows/test.yml b/submissions/mcp-hackathon/kele0929-venueclock/source/.github/workflows/test.yml new file mode 100644 index 0000000..e0c49d7 --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/source/.github/workflows/test.yml @@ -0,0 +1,13 @@ +name: test +on: + push: + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npm test diff --git a/submissions/mcp-hackathon/kele0929-venueclock/source/.gitignore b/submissions/mcp-hackathon/kele0929-venueclock/source/.gitignore new file mode 100644 index 0000000..a0dba1c --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/source/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +site/ +.DS_Store diff --git a/submissions/mcp-hackathon/kele0929-venueclock/source/LICENSE b/submissions/mcp-hackathon/kele0929-venueclock/source/LICENSE new file mode 100644 index 0000000..a7294c5 --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/source/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 kele0929 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/submissions/mcp-hackathon/kele0929-venueclock/source/README.md b/submissions/mcp-hackathon/kele0929-venueclock/source/README.md new file mode 100644 index 0000000..3e115b2 --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/source/README.md @@ -0,0 +1,99 @@ +# VenueClock + +Deterministic **market-session calendar** that AI agents can call. Given a venue and a timestamp (or a calendar date), it returns whether that market is in pre-market, regular hours, after-hours, holiday, weekend, or closed, plus the next open or close. + +This is an Open Innovation entry for the [X-Agent AI MCP Hackathon 2026](https://xagt.ai/hackathon?lang=en). It is **not** a trading bot, not a risk scorer, and not an on-chain security tool. + +- Live API origin: https://kele0929.github.io +- Source: https://github.com/kele0929/venueclock +- MCP tool list: https://kele0929.github.io/mcp/tools.json +- Local MCP stdio: `node src/mcp.js` + +## Why this is agent-useful + +Agents that schedule orders, fetch prices, or write “is the market open?” answers routinely hallucinate holidays, early closes, and DST. VenueClock answers from explicit calendars and session rules, with ISO-8601 timestamps and machine-readable phases. + +Supported venues: + +| id | MIC | Market | Timezone | +| --- | --- | --- | --- | +| `xnys` | XNYS | New York Stock Exchange | America/New_York | +| `xnas` | XNAS | Nasdaq Stock Market | America/New_York | +| `xlon` | XLON | London Stock Exchange | Europe/London | +| `crypto` | — | 24/7 crypto (synthetic) | UTC | + +US 2026 holidays and 13:00 ET early closes follow the Nasdaq / NYSE 2026 calendars. LSE 2026 dates follow UK bank holidays. Crypto is always in `regular`. + +## HTTP API + +All JSON, CORS `*`, no authentication. + +| Method | Path | Purpose | +| --- | --- | --- | +| GET | `/health.json` | `{ "status": "ok", "commit": "" }` | +| GET | `/.well-known/xagent-verification.json` | X-Agent deployment proof | +| GET | `/v1/venues.json` | Venue index | +| GET | `/v1/venues/{id}.json` | Rules, sessions, 2026 holidays | +| GET | `/v1/resolve/{id}/{YYYY-MM-DD}.json` | Full day schedule in the venue timezone | +| GET | `/v1/resolve?venue={id}&at={iso8601}` | Instant session state (Node server) | +| GET | `/mcp/tools.json` | MCP tool descriptors | + +The GitHub Pages deploy is a static snapshot of the same router used by `node src/server.js`. Instant query-string resolve is available on the Node server; the public Pages origin exposes the day-schedule capability plus health/proof. + +### Example + +```bash +curl -sS https://kele0929.github.io/v1/resolve/xnys/2026-09-04.json +``` + +Friday 4 September 2026 is a normal NYSE day: pre 04:00–09:30, regular 09:30–16:00, after-hours 16:00–20:00 America/New_York (EDT). + +```bash +curl -sS https://kele0929.github.io/v1/resolve/xnys/2026-09-07.json +``` + +Labor Day 2026: `status` is `holiday`, `sessions` is empty. + +Unknown venues return HTTP 404 JSON `{ "error": "not_found", ... }`. Invalid dates return HTTP 400. + +## Run locally + +Requires Node.js 18.17+. + +```bash +npm test +REVIEW_COMMIT=$(git rev-parse HEAD) npm start +# http://127.0.0.1:8787/health.json +# http://127.0.0.1:8787/v1/resolve?venue=xnys&at=2026-09-04T15:00:00Z +``` + +MCP stdio: + +```bash +node src/mcp.js +``` + +Generate the static GitHub Pages snapshot: + +```bash +REVIEW_COMMIT=$(git rev-parse HEAD) npm run generate -- --out ./site +``` + +## Deploy + +1. Push this repository to `https://github.com/kele0929/venueclock`. +2. Record `REVIEW_COMMIT=$(git rev-parse HEAD)`. +3. Generate the site with that commit and publish the `site/` tree to `https://github.com/kele0929/kele0929.github.io` on branch `main` (GitHub Pages user site). +4. Confirm: + - `GET https://kele0929.github.io/health.json` → `status=ok` and the same commit + - `GET https://kele0929.github.io/.well-known/xagent-verification.json` → slug `kele0929-venueclock` and the same commit + +The Pages origin is `https://kele0929.github.io`, so the well-known proof sits at the origin root as required by X-Agent. + +## Boundary + +VenueClock reports **session calendars**. It does not place orders, return prices, score wallets, detect exploits, or provide financial advice. Holiday tables cover **2026** (the review year). Other years use the same weekly hours and DST rules but only 2026 named holidays are encoded. + +## License + +MIT. Builder: kele0929. diff --git a/submissions/mcp-hackathon/kele0929-venueclock/source/openapi.json b/submissions/mcp-hackathon/kele0929-venueclock/source/openapi.json new file mode 100644 index 0000000..726fbaf --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/source/openapi.json @@ -0,0 +1,46 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "VenueClock", + "version": "1.0.0", + "description": "Agent-callable market session calendar." + }, + "servers": [{ "url": "https://kele0929.github.io" }], + "paths": { + "/health.json": { + "get": { + "summary": "Health and commit binding", + "responses": { "200": { "description": "ok" } } + } + }, + "/.well-known/xagent-verification.json": { + "get": { + "summary": "X-Agent deployment proof", + "responses": { "200": { "description": "ok" } } + } + }, + "/v1/venues.json": { + "get": { + "summary": "List venues", + "responses": { "200": { "description": "ok" } } + } + }, + "/v1/venues/{id}.json": { + "get": { + "summary": "Venue rules", + "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }], + "responses": { "200": { "description": "ok" }, "404": { "description": "unknown venue" } } + } + }, + "/v1/resolve/{id}/{date}.json": { + "get": { + "summary": "Day schedule", + "parameters": [ + { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }, + { "name": "date", "in": "path", "required": true, "schema": { "type": "string", "format": "date" } } + ], + "responses": { "200": { "description": "ok" }, "400": { "description": "bad date" }, "404": { "description": "unknown venue" } } + } + } + } +} diff --git a/submissions/mcp-hackathon/kele0929-venueclock/source/package.json b/submissions/mcp-hackathon/kele0929-venueclock/source/package.json new file mode 100644 index 0000000..dc4c9ae --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/source/package.json @@ -0,0 +1,17 @@ +{ + "name": "venueclock", + "version": "1.0.0", + "private": false, + "type": "module", + "description": "Agent-callable market session calendar: session state, holidays, and next open/close for NYSE, Nasdaq, LSE, and 24/7 crypto.", + "license": "MIT", + "engines": { + "node": ">=18.17" + }, + "scripts": { + "start": "node src/server.js", + "mcp": "node src/mcp.js", + "test": "node --test", + "generate": "node src/generate-site.js" + } +} diff --git a/submissions/mcp-hackathon/kele0929-venueclock/source/src/calendar.js b/submissions/mcp-hackathon/kele0929-venueclock/source/src/calendar.js new file mode 100644 index 0000000..b559754 --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/source/src/calendar.js @@ -0,0 +1,264 @@ +import { getVenue, listVenues, VENUES } from "./venues.js"; + +const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + +function pad(value) { + return String(value).padStart(2, "0"); +} + +function ymd(year, month, day) { + return `${year}-${pad(month)}-${pad(day)}`; +} + +function nthWeekday(year, month, weekday, n) { + const first = new Date(Date.UTC(year, month - 1, 1)); + const firstWeekday = first.getUTCDay(); + return 1 + ((weekday - firstWeekday + 7) % 7) + (n - 1) * 7; +} + +function lastWeekday(year, month, weekday) { + const last = new Date(Date.UTC(year, month, 0)).getUTCDate(); + const lastWeekdayValue = new Date(Date.UTC(year, month - 1, last)).getUTCDay(); + return last - ((lastWeekdayValue - weekday + 7) % 7); +} + +export function tzOffsetMin(tz, utcMs) { + if (tz === "UTC") return 0; + if (tz === "Asia/Tokyo") return 9 * 60; + if (tz === "Asia/Hong_Kong") return 8 * 60; + const year = new Date(utcMs).getUTCFullYear(); + if (tz === "America/New_York") { + const start = Date.UTC(year, 2, nthWeekday(year, 3, 0, 2), 7, 0, 0); + const end = Date.UTC(year, 10, nthWeekday(year, 11, 0, 1), 6, 0, 0); + return utcMs >= start && utcMs < end ? -4 * 60 : -5 * 60; + } + if (tz === "Europe/London") { + const start = Date.UTC(year, 2, lastWeekday(year, 3, 0), 1, 0, 0); + const end = Date.UTC(year, 9, lastWeekday(year, 10, 0), 1, 0, 0); + return utcMs >= start && utcMs < end ? 60 : 0; + } + throw new Error(`unsupported timezone: ${tz}`); +} + +export function localParts(tz, utcMs) { + const offsetMin = tzOffsetMin(tz, utcMs); + const local = new Date(utcMs + offsetMin * 60 * 1000); + const year = local.getUTCFullYear(); + const month = local.getUTCMonth() + 1; + const day = local.getUTCDate(); + const hour = local.getUTCHours(); + const minute = local.getUTCMinutes(); + const second = local.getUTCSeconds(); + return { + year, + month, + day, + hour, + minute, + second, + offsetMin, + ymd: ymd(year, month, day), + weekday: local.getUTCDay(), + hhmm: `${pad(hour)}:${pad(minute)}` + }; +} + +export function localToUtc(tz, year, month, day, hour, minute, second = 0) { + const asUtc = Date.UTC(year, month - 1, day, hour, minute, second); + let offsetMin = tzOffsetMin(tz, asUtc); + let utcMs = asUtc - offsetMin * 60 * 1000; + offsetMin = tzOffsetMin(tz, utcMs); + utcMs = asUtc - offsetMin * 60 * 1000; + return utcMs; +} + +function formatOffset(offsetMin) { + const sign = offsetMin >= 0 ? "+" : "-"; + const absolute = Math.abs(offsetMin); + return `${sign}${pad(Math.floor(absolute / 60))}:${pad(absolute % 60)}`; +} + +export function formatInstant(tz, utcMs) { + const parts = localParts(tz, utcMs); + return `${ymd(parts.year, parts.month, parts.day)}T${pad(parts.hour)}:${pad(parts.minute)}:${pad(parts.second)}${formatOffset(parts.offsetMin)}`; +} + +function parseHHmm(value) { + if (value === "24:00") return { hour: 24, minute: 0 }; + const [hour, minute] = value.split(":").map(Number); + return { hour, minute }; +} + +function minutesOfDay(hhmm) { + const { hour, minute } = parseHHmm(hhmm); + return hour * 60 + minute; +} + +function addDays(year, month, day, delta) { + const next = new Date(Date.UTC(year, month - 1, day + delta)); + return { year: next.getUTCFullYear(), month: next.getUTCMonth() + 1, day: next.getUTCDate() }; +} + +function sessionsForDate(venue, dateStr) { + const holiday = venue.holidays[dateStr] ?? null; + if (holiday?.type === "closed") { + return { holiday, sessions: [] }; + } + const sessions = venue.sessions.map((session) => ({ ...session })); + if (holiday?.type === "early_close") { + const regular = sessions.find((session) => session.id === "regular"); + if (regular) { + regular.end = holiday.regularEnd; + regular.earlyClose = true; + } + const post = sessions.find((session) => session.id === "post"); + if (post && regular) { + post.start = holiday.regularEnd; + } + return { holiday, sessions: sessions.filter((session) => minutesOfDay(session.start) < minutesOfDay(session.end)) }; + } + return { holiday, sessions }; +} + +function localDateFromYmd(dateStr) { + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr); + if (!match) return null; + return { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) }; +} + +export function daySchedule(venueId, dateStr) { + const venue = getVenue(venueId); + if (!venue) return { error: "unknown_venue", message: `Unknown venue: ${venueId}` }; + const parsed = localDateFromYmd(dateStr); + if (!parsed) return { error: "invalid_date", message: "date must be YYYY-MM-DD in the venue timezone" }; + + const weekday = new Date(Date.UTC(parsed.year, parsed.month - 1, parsed.day)).getUTCDay(); + const isWeekend = venue.weekend.includes(weekday); + const { holiday, sessions } = isWeekend ? { holiday: null, sessions: [] } : sessionsForDate(venue, dateStr); + + const mapped = sessions.map((session) => { + const start = parseHHmm(session.start); + const end = parseHHmm(session.end); + const endDate = end.hour === 24 ? addDays(parsed.year, parsed.month, parsed.day, 1) : parsed; + const endHour = end.hour === 24 ? 0 : end.hour; + const openUtc = localToUtc(venue.timezone, parsed.year, parsed.month, parsed.day, start.hour, start.minute); + const closeUtc = localToUtc(venue.timezone, endDate.year, endDate.month, endDate.day, endHour, end.minute); + return { + id: session.id, + label: session.label, + open: formatInstant(venue.timezone, openUtc), + close: formatInstant(venue.timezone, closeUtc), + openUtc: new Date(openUtc).toISOString(), + closeUtc: new Date(closeUtc).toISOString(), + earlyClose: Boolean(session.earlyClose) + }; + }); + + let status = "open"; + if (isWeekend) status = "weekend"; + else if (holiday?.type === "closed") status = "holiday"; + else if (mapped.length === 0) status = "closed"; + + return { + venue: venue.id, + mic: venue.mic, + name: venue.name, + date: dateStr, + timezone: venue.timezone, + weekday: WEEKDAYS[weekday], + closed: mapped.length === 0, + status, + holiday: holiday ? { type: holiday.type, name: holiday.name } : null, + sessions: mapped + }; +} + +function nextOpenAfter(venue, fromUtc) { + for (let offset = 0; offset <= 10; offset += 1) { + const cursor = localParts(venue.timezone, fromUtc + offset * 24 * 60 * 60 * 1000); + const schedule = daySchedule(venue.id, cursor.ymd); + for (const session of schedule.sessions) { + const openMs = Date.parse(session.openUtc); + if (openMs > fromUtc) { + return { at: session.open, atUtc: session.openUtc, session: session.id, date: schedule.date }; + } + } + } + return null; +} + +export function resolveSession(venueId, instant) { + const venue = getVenue(venueId); + if (!venue) return { error: "unknown_venue", message: `Unknown venue: ${venueId}` }; + const utcMs = instant instanceof Date ? instant.getTime() : Date.parse(instant); + if (!Number.isFinite(utcMs)) return { error: "invalid_time", message: "at must be an ISO-8601 timestamp" }; + + const local = localParts(venue.timezone, utcMs); + const schedule = daySchedule(venue.id, local.ymd); + const nowMinutes = local.hour * 60 + local.minute + local.second / 60; + + let current = null; + for (const session of schedule.sessions) { + const openMs = Date.parse(session.openUtc); + const closeMs = Date.parse(session.closeUtc); + if (utcMs >= openMs && utcMs < closeMs) { + current = session; + break; + } + } + + let phase = "closed"; + if (schedule.status === "weekend") phase = "weekend"; + else if (schedule.status === "holiday") phase = "holiday"; + else if (current) phase = current.id; + + const nextChange = current + ? { type: "close", session: current.id, at: current.close, atUtc: current.closeUtc } + : (() => { + const upcoming = schedule.sessions.find((session) => Date.parse(session.openUtc) > utcMs); + if (upcoming) return { type: "open", session: upcoming.id, at: upcoming.open, atUtc: upcoming.openUtc }; + const next = nextOpenAfter(venue, utcMs); + return next ? { type: "open", session: next.session, at: next.at, atUtc: next.atUtc } : null; + })(); + + return { + venue: venue.id, + mic: venue.mic, + name: venue.name, + at: formatInstant(venue.timezone, utcMs), + atUtc: new Date(utcMs).toISOString(), + localDate: local.ymd, + localTime: `${pad(local.hour)}:${pad(local.minute)}:${pad(local.second)}`, + timezone: venue.timezone, + utcOffset: formatOffset(local.offsetMin), + phase, + inSession: Boolean(current), + session: current, + nextChange, + holiday: schedule.holiday, + weekday: schedule.weekday, + minutesOfLocalDay: Math.floor(nowMinutes) + }; +} + +export function venueDescriptor(venueId) { + const venue = getVenue(venueId); + if (!venue) return null; + const year = 2026; + const holidays = Object.entries(venue.holidays) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([date, info]) => ({ date, ...info })); + return { + id: venue.id, + mic: venue.mic, + name: venue.name, + timezone: venue.timezone, + weekend: venue.weekend, + sessions: venue.sessions, + holidays, + year, + sources: venue.sources + }; +} + +export { getVenue, listVenues, VENUES }; diff --git a/submissions/mcp-hackathon/kele0929-venueclock/source/src/generate-site.js b/submissions/mcp-hackathon/kele0929-venueclock/source/src/generate-site.js new file mode 100644 index 0000000..a4c2445 --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/source/src/generate-site.js @@ -0,0 +1,87 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { listVenues } from "./calendar.js"; +import { SLUG, route } from "./http.js"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); +const outDir = process.argv.includes("--out") + ? process.argv[process.argv.indexOf("--out") + 1] + : join(ROOT, "site"); +const commit = process.env.REVIEW_COMMIT; +if (!commit || !/^[a-f0-9]{40}$/i.test(commit)) { + throw new Error("REVIEW_COMMIT must be the 40-character source commit SHA"); +} + +async function writeJson(relPath, body) { + const full = join(outDir, relPath); + await mkdir(dirname(full), { recursive: true }); + await writeFile(full, `${JSON.stringify(body)}\n`, "utf8"); +} + +function bodyOf(path) { + const result = route(new URL(path, "http://venueclock.local"), { commit }); + if (result.status !== 200) throw new Error(`${path} -> ${result.status} ${JSON.stringify(result.body)}`); + return result.body; +} + +const venues = listVenues(); +const dates = []; +for (let cursor = new Date(Date.UTC(2026, 0, 1)); cursor.getUTCFullYear() === 2026; cursor.setUTCDate(cursor.getUTCDate() + 1)) { + const y = cursor.getUTCFullYear(); + const m = String(cursor.getUTCMonth() + 1).padStart(2, "0"); + const d = String(cursor.getUTCDate()).padStart(2, "0"); + dates.push(`${y}-${m}-${d}`); +} + +await mkdir(outDir, { recursive: true }); +await writeFile(join(outDir, ".nojekyll"), "", "utf8"); +await writeJson("health.json", bodyOf("/health.json")); +await writeJson(".well-known/xagent-verification.json", bodyOf("/.well-known/xagent-verification.json")); +await writeJson("v1/index.json", bodyOf("/v1")); +await writeJson("v1/venues.json", bodyOf("/v1/venues.json")); +await writeJson("mcp/tools.json", bodyOf("/mcp/tools.json")); + +for (const venue of venues) { + await writeJson(`v1/venues/${venue.id}.json`, bodyOf(`/v1/venues/${venue.id}.json`)); + for (const date of dates) { + await writeJson(`v1/resolve/${venue.id}/${date}.json`, bodyOf(`/v1/resolve/${venue.id}/${date}.json`)); + } +} + +const index = ` + + + + + VenueClock — agent-callable market session calendar + + + +
+

X-Agent MCP Hackathon 2026 · Open Innovation · ${SLUG}

+

VenueClock

+

Deterministic market-session calendar for AI agents. Returns pre-market, regular, after-hours, holiday, weekend, and the next open or close for NYSE, Nasdaq, LSE, and 24/7 crypto.

+

Commit ${commit}

+

Live calls

+
curl -sS https://kele0929.github.io/health.json
+curl -sS https://kele0929.github.io/.well-known/xagent-verification.json
+curl -sS https://kele0929.github.io/v1/venues.json
+curl -sS https://kele0929.github.io/v1/resolve/xnys/2026-09-04.json
+

MCP tools: /mcp/tools.json. Local stdio server: node src/mcp.js.

+

Source: github.com/kele0929/venueclock

+
+ + +`; +await writeFile(join(outDir, "index.html"), index, "utf8"); +process.stdout.write(`wrote ${outDir} commit=${commit} venues=${venues.length} days=${dates.length}\n`); diff --git a/submissions/mcp-hackathon/kele0929-venueclock/source/src/http.js b/submissions/mcp-hackathon/kele0929-venueclock/source/src/http.js new file mode 100644 index 0000000..9c6e7da --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/source/src/http.js @@ -0,0 +1,137 @@ +import { daySchedule, listVenues, resolveSession, venueDescriptor } from "./calendar.js"; + +export const SLUG = "kele0929-venueclock"; + +export function healthBody(commit) { + return { status: "ok", commit }; +} + +export function proofBody(commit) { + return { schemaVersion: 1, slug: SLUG, commit }; +} + +function json(status, body) { + return { status, headers: { "content-type": "application/json; charset=utf-8" }, body }; +} + +function notFound(message) { + return json(404, { error: "not_found", message }); +} + +function badRequest(error, message) { + return json(400, { error, message }); +} + +export function route(url, { commit, now = () => new Date() } = {}) { + const parsed = url instanceof URL ? url : new URL(url, "http://venueclock.local"); + let pathname = parsed.pathname; + if (pathname.length > 1 && pathname.endsWith("/")) pathname = pathname.slice(0, -1); + + if (pathname === "/health" || pathname === "/health.json") { + return json(200, healthBody(commit)); + } + if (pathname === "/.well-known/xagent-verification.json") { + return json(200, proofBody(commit)); + } + if (pathname === "/v1" || pathname === "/v1/index.json") { + return json(200, { + name: "VenueClock", + slug: SLUG, + commit, + endpoints: { + health: "/health.json", + proof: "/.well-known/xagent-verification.json", + venues: "/v1/venues.json", + venue: "/v1/venues/{id}.json", + day: "/v1/resolve/{id}/{YYYY-MM-DD}.json", + instant: "/v1/resolve?venue={id}&at={iso8601}" + } + }); + } + if (pathname === "/v1/venues" || pathname === "/v1/venues.json") { + return json(200, { venues: listVenues() }); + } + + const venueFile = /^\/v1\/venues\/([a-z0-9]+?)(?:\.json)?$/.exec(pathname); + if (venueFile) { + const descriptor = venueDescriptor(venueFile[1]); + if (!descriptor) return notFound(`Unknown venue: ${venueFile[1]}`); + return json(200, descriptor); + } + + const dayFile = /^\/v1\/resolve\/([a-z0-9]+)\/(\d{4}-\d{2}-\d{2})(?:\.json)?$/.exec(pathname); + if (dayFile) { + const result = daySchedule(dayFile[1], dayFile[2]); + if (result.error) return result.error === "unknown_venue" ? notFound(result.message) : badRequest(result.error, result.message); + return json(200, result); + } + + if (pathname === "/v1/resolve") { + const venue = parsed.searchParams.get("venue"); + const date = parsed.searchParams.get("date"); + const at = parsed.searchParams.get("at"); + if (!venue) return badRequest("missing_venue", "query parameter venue is required"); + if (date && !at) { + const result = daySchedule(venue, date); + if (result.error) return result.error === "unknown_venue" ? notFound(result.message) : badRequest(result.error, result.message); + return json(200, result); + } + const result = resolveSession(venue, at || now()); + if (result.error) { + if (result.error === "unknown_venue") return notFound(result.message); + return badRequest(result.error, result.message); + } + return json(200, result); + } + + if (pathname === "/mcp/tools.json") { + return json(200, { + protocol: "mcp", + tools: [ + { + name: "list_venues", + description: "List supported market venues (MIC, timezone).", + inputSchema: { type: "object", properties: {}, additionalProperties: false } + }, + { + name: "get_venue", + description: "Return session rules, timezone, and 2026 holidays for a venue.", + inputSchema: { + type: "object", + additionalProperties: false, + required: ["venue"], + properties: { venue: { type: "string", description: "Venue id or MIC, e.g. xnys or XNYS" } } + } + }, + { + name: "resolve_session", + description: "Return whether a venue is in pre-market, regular, after-hours, holiday, or weekend at an instant, plus the next open or close.", + inputSchema: { + type: "object", + additionalProperties: false, + required: ["venue"], + properties: { + venue: { type: "string" }, + at: { type: "string", description: "ISO-8601 timestamp. Defaults to now on the live HTTP server." }, + date: { type: "string", description: "YYYY-MM-DD in the venue timezone. Returns the full day schedule instead of an instant." } + } + } + } + ] + }); + } + + return notFound(`No route for ${pathname}`); +} + +export function send(res, result) { + const payload = `${JSON.stringify(result.body)}\n`; + res.writeHead(result.status, { + "content-type": "application/json; charset=utf-8", + "cache-control": "public, max-age=60", + "access-control-allow-origin": "*", + "x-source-commit": result.body.commit ?? "", + "content-length": Buffer.byteLength(payload) + }); + res.end(payload); +} diff --git a/submissions/mcp-hackathon/kele0929-venueclock/source/src/mcp.js b/submissions/mcp-hackathon/kele0929-venueclock/source/src/mcp.js new file mode 100644 index 0000000..52d8967 --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/source/src/mcp.js @@ -0,0 +1,102 @@ +#!/usr/bin/env node +/** + * Minimal MCP stdio server (JSON-RPC 2.0, newline-delimited). + * Tools: list_venues, get_venue, resolve_session. + */ +import { createInterface } from "node:readline"; +import { daySchedule, listVenues, resolveSession, venueDescriptor } from "./calendar.js"; + +const PROTOCOL_VERSION = "2024-11-05"; + +function ok(id, result) { + return { jsonrpc: "2.0", id, result }; +} + +function fail(id, code, message) { + return { jsonrpc: "2.0", id, error: { code, message } }; +} + +function toolText(id, value) { + return ok(id, { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] }); +} + +function handle(message) { + const { id, method, params } = message; + if (method === "initialize") { + return ok(id, { + protocolVersion: PROTOCOL_VERSION, + serverInfo: { name: "venueclock", version: "1.0.0" }, + capabilities: { tools: {} } + }); + } + if (method === "notifications/initialized" || method === "notifications/cancelled") { + return null; + } + if (method === "ping") return ok(id, {}); + if (method === "tools/list") { + return ok(id, { + tools: [ + { + name: "list_venues", + description: "List supported market venues (MIC, timezone).", + inputSchema: { type: "object", properties: {}, additionalProperties: false } + }, + { + name: "get_venue", + description: "Return session rules, timezone, and 2026 holidays for a venue.", + inputSchema: { + type: "object", + additionalProperties: false, + required: ["venue"], + properties: { venue: { type: "string" } } + } + }, + { + name: "resolve_session", + description: "Return session state for a venue at an instant, or a full day schedule when date is set.", + inputSchema: { + type: "object", + additionalProperties: false, + required: ["venue"], + properties: { + venue: { type: "string" }, + at: { type: "string" }, + date: { type: "string" } + } + } + } + ] + }); + } + if (method === "tools/call") { + const name = params?.name; + const args = params?.arguments ?? {}; + if (name === "list_venues") return toolText(id, { venues: listVenues() }); + if (name === "get_venue") { + const descriptor = venueDescriptor(args.venue); + if (!descriptor) return toolText(id, { error: "unknown_venue", message: `Unknown venue: ${args.venue}` }); + return toolText(id, descriptor); + } + if (name === "resolve_session") { + if (args.date && !args.at) return toolText(id, daySchedule(args.venue, args.date)); + return toolText(id, resolveSession(args.venue, args.at || new Date())); + } + return fail(id, -32601, `Unknown tool: ${name}`); + } + if (id === undefined) return null; + return fail(id, -32601, `Unknown method: ${method}`); +} + +const rl = createInterface({ input: process.stdin, crlfDelay: Infinity }); +rl.on("line", (line) => { + if (!line.trim()) return; + let message; + try { + message = JSON.parse(line); + } catch { + process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", error: { code: -32700, message: "parse error" } })}\n`); + return; + } + const response = handle(message); + if (response) process.stdout.write(`${JSON.stringify(response)}\n`); +}); diff --git a/submissions/mcp-hackathon/kele0929-venueclock/source/src/server.js b/submissions/mcp-hackathon/kele0929-venueclock/source/src/server.js new file mode 100644 index 0000000..ec1ead4 --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/source/src/server.js @@ -0,0 +1,52 @@ +import { createServer } from "node:http"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { route, send } from "./http.js"; + +const ROOT = dirname(fileURLToPath(import.meta.url)); + +function loadCommit() { + if (process.env.REVIEW_COMMIT) return process.env.REVIEW_COMMIT; + try { + return readFileSync(join(ROOT, "..", "COMMIT"), "utf8").trim(); + } catch { + return "0".repeat(40); + } +} + +const commit = loadCommit(); +const port = Number(process.env.PORT || 8787); + +const server = createServer((req, res) => { + try { + const host = req.headers.host || "127.0.0.1"; + const url = new URL(req.url || "/", `http://${host}`); + if (req.method === "OPTIONS") { + res.writeHead(204, { + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET, OPTIONS", + "access-control-allow-headers": "content-type" + }); + res.end(); + return; + } + if (req.method !== "GET" && req.method !== "HEAD") { + send(res, { status: 405, body: { error: "method_not_allowed", message: "GET only" } }); + return; + } + const result = route(url, { commit }); + if (req.method === "HEAD") { + res.writeHead(result.status, { "content-type": "application/json; charset=utf-8" }); + res.end(); + return; + } + send(res, result); + } catch (error) { + send(res, { status: 500, body: { error: "internal", message: error instanceof Error ? error.message : String(error) } }); + } +}); + +server.listen(port, () => { + process.stdout.write(`VenueClock listening on http://127.0.0.1:${port} commit=${commit}\n`); +}); diff --git a/submissions/mcp-hackathon/kele0929-venueclock/source/src/venues.js b/submissions/mcp-hackathon/kele0929-venueclock/source/src/venues.js new file mode 100644 index 0000000..dfa2742 --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/source/src/venues.js @@ -0,0 +1,108 @@ +/** + * Venue session rules for 2026. + * + * US equity holidays / early closes: Nasdaq 2026 holiday calendar + * (https://www.nasdaqtrader.com/trader.aspx?id=Calendar) and NYSE Group + * 2025–2027 holiday schedule as reported by the exchanges. + * + * LSE dates follow UK bank holidays for 2026 (GOV.UK). Hours are the + * London Stock Exchange electronic order book: 08:00–16:30 Europe/London. + * + * CRYPTO is a synthetic 24/7 venue (UTC). It is not an ISO MIC. + */ + +const US_EQUITY_HOLIDAYS_2026 = { + "2026-01-01": { type: "closed", name: "New Year's Day" }, + "2026-01-19": { type: "closed", name: "Martin Luther King, Jr. Day" }, + "2026-02-16": { type: "closed", name: "Washington's Birthday (Presidents' Day)" }, + "2026-04-03": { type: "closed", name: "Good Friday" }, + "2026-05-25": { type: "closed", name: "Memorial Day" }, + "2026-06-19": { type: "closed", name: "Juneteenth National Independence Day" }, + "2026-07-03": { type: "closed", name: "Independence Day (observed)" }, + "2026-09-07": { type: "closed", name: "Labor Day" }, + "2026-11-26": { type: "closed", name: "Thanksgiving Day" }, + "2026-11-27": { type: "early_close", name: "Day after Thanksgiving", regularEnd: "13:00" }, + "2026-12-24": { type: "early_close", name: "Christmas Eve", regularEnd: "13:00" }, + "2026-12-25": { type: "closed", name: "Christmas Day" } +}; + +const LSE_HOLIDAYS_2026 = { + "2026-01-01": { type: "closed", name: "New Year's Day" }, + "2026-04-03": { type: "closed", name: "Good Friday" }, + "2026-04-06": { type: "closed", name: "Easter Monday" }, + "2026-05-04": { type: "closed", name: "Early May bank holiday" }, + "2026-05-25": { type: "closed", name: "Spring bank holiday" }, + "2026-08-31": { type: "closed", name: "Summer bank holiday" }, + "2026-12-25": { type: "closed", name: "Christmas Day" }, + "2026-12-28": { type: "closed", name: "Boxing Day (substitute)" } +}; + +export const VENUES = { + xnys: { + id: "xnys", + mic: "XNYS", + name: "New York Stock Exchange", + timezone: "America/New_York", + weekend: [0, 6], + sessions: [ + { id: "pre", label: "pre-market", start: "04:00", end: "09:30" }, + { id: "regular", label: "regular", start: "09:30", end: "16:00" }, + { id: "post", label: "after-hours", start: "16:00", end: "20:00" } + ], + holidays: US_EQUITY_HOLIDAYS_2026, + sources: [ + "https://www.nasdaqtrader.com/trader.aspx?id=Calendar", + "https://www.nyse.com/" + ] + }, + xnas: { + id: "xnas", + mic: "XNAS", + name: "Nasdaq Stock Market", + timezone: "America/New_York", + weekend: [0, 6], + sessions: [ + { id: "pre", label: "pre-market", start: "04:00", end: "09:30" }, + { id: "regular", label: "regular", start: "09:30", end: "16:00" }, + { id: "post", label: "after-hours", start: "16:00", end: "20:00" } + ], + holidays: US_EQUITY_HOLIDAYS_2026, + sources: ["https://www.nasdaqtrader.com/trader.aspx?id=Calendar"] + }, + xlon: { + id: "xlon", + mic: "XLON", + name: "London Stock Exchange", + timezone: "Europe/London", + weekend: [0, 6], + sessions: [{ id: "regular", label: "regular", start: "08:00", end: "16:30" }], + holidays: LSE_HOLIDAYS_2026, + sources: ["https://www.gov.uk/bank-holidays"] + }, + crypto: { + id: "crypto", + mic: null, + name: "24/7 crypto (synthetic UTC venue)", + timezone: "UTC", + weekend: [], + sessions: [{ id: "regular", label: "continuous", start: "00:00", end: "24:00" }], + holidays: {}, + sources: [] + } +}; + +export function listVenues() { + return Object.values(VENUES).map((venue) => ({ + id: venue.id, + mic: venue.mic, + name: venue.name, + timezone: venue.timezone + })); +} + +export function getVenue(raw) { + if (typeof raw !== "string" || !raw.trim()) return null; + const key = raw.trim().toLowerCase(); + if (VENUES[key]) return VENUES[key]; + return Object.values(VENUES).find((venue) => venue.mic && venue.mic.toLowerCase() === key) ?? null; +} diff --git a/submissions/mcp-hackathon/kele0929-venueclock/source/test/calendar.test.js b/submissions/mcp-hackathon/kele0929-venueclock/source/test/calendar.test.js new file mode 100644 index 0000000..a806eb2 --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/source/test/calendar.test.js @@ -0,0 +1,97 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { daySchedule, resolveSession, tzOffsetMin } from "../src/calendar.js"; +import { route } from "../src/http.js"; + +const COMMIT = "0123456789abcdef0123456789abcdef01234567"; + +test("US Eastern DST around 2026-03-08 and 2026-11-01", () => { + const before = Date.parse("2026-03-08T06:59:00Z"); + const after = Date.parse("2026-03-08T07:00:00Z"); + assert.equal(tzOffsetMin("America/New_York", before), -5 * 60); + assert.equal(tzOffsetMin("America/New_York", after), -4 * 60); + const fallBefore = Date.parse("2026-11-01T05:59:00Z"); + const fallAfter = Date.parse("2026-11-01T06:00:00Z"); + assert.equal(tzOffsetMin("America/New_York", fallBefore), -4 * 60); + assert.equal(tzOffsetMin("America/New_York", fallAfter), -5 * 60); +}); + +test("NYSE closed New Year's Day 2026", () => { + const day = daySchedule("xnys", "2026-01-01"); + assert.equal(day.status, "holiday"); + assert.equal(day.sessions.length, 0); + const resolved = resolveSession("XNYS", "2026-01-01T15:00:00Z"); + assert.equal(resolved.phase, "holiday"); +}); + +test("NYSE Friday 2026-09-04 sessions", () => { + const day = daySchedule("xnys", "2026-09-04"); + assert.equal(day.closed, false); + assert.equal(day.weekday, "Friday"); + assert.equal(day.sessions.map((s) => s.id).join(","), "pre,regular,post"); + assert.equal(resolveSession("xnys", "2026-09-04T13:00:00Z").phase, "pre"); + assert.equal(resolveSession("xnys", "2026-09-04T15:00:00Z").phase, "regular"); + assert.equal(resolveSession("xnys", "2026-09-04T20:30:00Z").phase, "post"); + assert.equal(resolveSession("xnys", "2026-09-05T01:00:00Z").phase, "closed"); +}); + +test("NYSE Labor Day 2026-09-07 is a holiday", () => { + const day = daySchedule("xnys", "2026-09-07"); + assert.equal(day.status, "holiday"); + assert.equal(day.holiday.name, "Labor Day"); +}); + +test("NYSE weekend", () => { + const saturday = daySchedule("xnys", "2026-09-05"); + assert.equal(saturday.status, "weekend"); + assert.equal(resolveSession("xnys", "2026-09-05T15:00:00Z").phase, "weekend"); +}); + +test("NYSE early close 2026-11-27", () => { + const day = daySchedule("xnas", "2026-11-27"); + assert.equal(day.holiday.type, "early_close"); + const regular = day.sessions.find((s) => s.id === "regular"); + assert.match(regular.close, /13:00:00-05:00/); + assert.equal(resolveSession("xnas", "2026-11-27T16:00:00Z").phase, "regular"); + assert.equal(resolveSession("xnas", "2026-11-27T18:30:00Z").phase, "post"); +}); + +test("LSE regular session in BST", () => { + const resolved = resolveSession("xlon", "2026-09-04T09:00:00Z"); + assert.equal(resolved.phase, "regular"); + assert.equal(resolved.utcOffset, "+01:00"); +}); + +test("crypto is always regular", () => { + assert.equal(resolveSession("crypto", "2026-09-05T03:00:00Z").phase, "regular"); + assert.equal(daySchedule("crypto", "2026-12-25").closed, false); +}); + +test("unknown venue and bad date", () => { + assert.equal(daySchedule("xyz", "2026-09-04").error, "unknown_venue"); + assert.equal(daySchedule("xnys", "09/04/2026").error, "invalid_date"); + assert.equal(resolveSession("xnys", "not-a-time").error, "invalid_time"); +}); + +test("HTTP health and proof bind the commit", () => { + const health = route("/health.json", { commit: COMMIT }); + assert.equal(health.status, 200); + assert.deepEqual(health.body, { status: "ok", commit: COMMIT }); + const proof = route("/.well-known/xagent-verification.json", { commit: COMMIT }); + assert.equal(proof.body.schemaVersion, 1); + assert.equal(proof.body.slug, "kele0929-venueclock"); + assert.equal(proof.body.commit, COMMIT); +}); + +test("HTTP capability routes", () => { + const venues = route("/v1/venues.json", { commit: COMMIT }); + assert.equal(venues.status, 200); + assert.ok(venues.body.venues.some((v) => v.id === "xnys")); + const day = route("/v1/resolve/xnys/2026-09-04.json", { commit: COMMIT }); + assert.equal(day.status, 200); + assert.equal(day.body.weekday, "Friday"); + const instant = route("/v1/resolve?venue=XNYS&at=2026-09-04T15:00:00Z", { commit: COMMIT }); + assert.equal(instant.body.phase, "regular"); + const missing = route("/v1/resolve/nope/2026-09-04.json", { commit: COMMIT }); + assert.equal(missing.status, 404); +}); diff --git a/submissions/mcp-hackathon/kele0929-venueclock/submission.json b/submissions/mcp-hackathon/kele0929-venueclock/submission.json new file mode 100644 index 0000000..ff27d49 --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/submission.json @@ -0,0 +1,10 @@ +{ + "schemaVersion": 1, + "name": "VenueClock", + "slug": "kele0929-venueclock", + "sourceRepository": "https://github.com/kele0929/venueclock", + "reviewCommit": "d1b84e00cb87b7e8811a020f497855b67bc00303", + "apiBaseUrl": "https://kele0929.github.io/v1", + "healthCheckUrl": "https://kele0929.github.io/health.json", + "deploymentProofUrl": "https://kele0929.github.io/.well-known/xagent-verification.json" +} diff --git a/submissions/mcp-hackathon/kele0929-venueclock/verification/README.md b/submissions/mcp-hackathon/kele0929-venueclock/verification/README.md new file mode 100644 index 0000000..339530b --- /dev/null +++ b/submissions/mcp-hackathon/kele0929-venueclock/verification/README.md @@ -0,0 +1,55 @@ +# Verification evidence + +## Prerequisites + +- Review commit: `d1b84e00cb87b7e8811a020f497855b67bc00303` +- API base URL: `https://kele0929.github.io/v1` +- Authentication: none + +## 1. Health check + +```bash +curl --fail --silent --show-error https://kele0929.github.io/health.json +``` + +Expected response: + +```json +{"status":"ok","commit":"d1b84e00cb87b7e8811a020f497855b67bc00303"} +``` + +## 2. Deployment proof + +```bash +curl --fail --silent --show-error https://kele0929.github.io/.well-known/xagent-verification.json +``` + +Expected response: + +```json +{"schemaVersion":1,"slug":"kele0929-venueclock","commit":"d1b84e00cb87b7e8811a020f497855b67bc00303"} +``` + +## 3. Capability call + +```bash +curl --fail --silent --show-error https://kele0929.github.io/v1/resolve/xnys/2026-09-04.json +``` + +Expected success (Friday, regular session 09:30–16:00 America/New_York): + +```json +{"venue":"xnys","mic":"XNYS","name":"New York Stock Exchange","date":"2026-09-04","timezone":"America/New_York","weekday":"Friday","closed":false,"status":"open","holiday":null,"sessions":[{"id":"pre","label":"pre-market","open":"2026-09-04T04:00:00-04:00","close":"2026-09-04T09:30:00-04:00","openUtc":"2026-09-04T08:00:00.000Z","closeUtc":"2026-09-04T13:30:00.000Z","earlyClose":false},{"id":"regular","label":"regular","open":"2026-09-04T09:30:00-04:00","close":"2026-09-04T16:00:00-04:00","openUtc":"2026-09-04T13:30:00.000Z","closeUtc":"2026-09-04T20:00:00.000Z","earlyClose":false},{"id":"post","label":"after-hours","open":"2026-09-04T16:00:00-04:00","close":"2026-09-04T20:00:00-04:00","openUtc":"2026-09-04T20:00:00.000Z","closeUtc":"2026-09-05T00:00:00.000Z","earlyClose":false}]} +``` + +Safe failure (Labor Day 2026, market closed — still JSON, HTTP 200): + +```bash +curl --fail --silent --show-error https://kele0929.github.io/v1/resolve/xnys/2026-09-07.json +``` + +```json +{"venue":"xnys","mic":"XNYS","name":"New York Stock Exchange","date":"2026-09-07","timezone":"America/New_York","weekday":"Monday","closed":true,"status":"holiday","holiday":{"type":"closed","name":"Labor Day"},"sessions":[]} +``` + +On the Node server (`npm start`), unknown venues return HTTP 404 JSON `{"error":"not_found",...}` and invalid dates return HTTP 400 JSON. Those dynamic error bodies are not files on GitHub Pages.