Yoya + Claude Code: inspect booking tools before confirming a meeting
Check Yoya's public MCP connection and advertised confirmation fields, then review availability and a booking preview before any separately approved real meeting.
Run this workflow
CI-verified, 7/7 fixtures passing.
Build this with your agent
One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.
Intended Use
Check that a compatible public Yoya MCP server exposes the expected booking contract, then use a controlled test page to manually review a meeting proposal. Every real booking, reschedule and cancellation requires explicit user approval; setup verification does not extend to those actions.
Not for
- Claiming real booking, preview side-effect freedom, human consent enforcement, OAuth, calendar sync, email delivery or brief quality was tested
- Automatically creating, canceling or rescheduling meetings from CI or interpreting a confirmation token as independent proof of human approval
- Paid meetings, using someone else's live booking page for testing, or publishing private management tokens and guest information
- Treating public guest discovery as host-account access or treating all tools on this server as read-only
- Calling the hosted Yoya service open source or calling a live-network smoke check an offline or complete MCP-conformance test
The Stack
Tested Against
Yoya public MCP endpoint@https://yoya.ai/api/mcp/book, checked 2026-11-04MCP Streamable HTTP@2025-03-26; JSON and SSE response parsingyoya-booking@2.0.0 observed during review; compatible schema is checked on each runPython 3 and curl; no Claude Code execution or authenticated booking testSide effects & data flow
- Network
- Three bounded protocol requests to https://yoya.ai/api/mcp/book, plus bounded tools/list pagination if needed; no tools/call, Only an intentional manual booking can reserve time or send notifications
- Writes
- Temporary response files removed by the smoke checker, Optional claude mcp add changes the selected project's MCP configuration; approved real meetings change hosted booking/calendar state
- Credentials
- None for protocol discovery; a Claude account for optional client use and the user's own Yoya OAuth only for separate host-management actions
Data privacy
- Yoya public MCP endpoint ← Protocol version, a generic client identifier and tool-list requests only; no guest email, host token or invoice (retention: Per Yoya's service logging and privacy policies)
- Yoya, its providers and the chosen AI client only during a manual booking ← Guest details, meeting purpose, time and any context you explicitly submit; keep private manage links out of shared results (retention: Per your client settings and Yoya's policy; bookings/briefs may outlive chat-transcript expiry)
Prerequisites
- Python 3, curl and internet access for the public smoke check; no key or account is sent
- Optional manual flow: a Claude Code installation/account supporting remote HTTP MCP; Claude costs are separate
- A host-controlled test booking page, future free-of-charge slot, correct IANA time zone and guest inbox you control for any actual booking test
- Google Calendar/Meet or Zoom connected in Yoya if you choose to test them; adding MCP does not connect these providers
- Yoya lists one booking page and 10 monthly bookings on Free; check current pricing and quotas before use
Steps
- 1
Run the public connection and discovery smoke check
Run this before adding the server to an agent. It accepts JSON or SSE, validates response IDs and HTTP errors, negotiates a supported protocol, sends the initialized notification, and inspects only tool schemas. It permits no tools/call method. The response parser also rejects five malformed/error cases offline. Internet access is required and an outage fails the check. Passing says the public setup contract was observed; no booking or security enforcement is tested.
#!/bin/sh # Public protocol discovery only. No tools/call, guest details, credentials, or booking writes. set -eu python3 -I <<'PY' import json import pathlib import subprocess import tempfile ENDPOINT = 'https://yoya.ai/api/mcp/book' VERSION = '2025-03-26' def decode_rpc(body, content_type, request_id): if content_type.split(';')[0].strip() == 'application/json': values = [json.loads(body)] elif content_type.split(';')[0].strip() == 'text/event-stream': values, data = [], [] for line in body.replace('\r\n', '\n').split('\n') + ['']: if not line: if data: values.append(json.loads('\n'.join(data))) data = [] elif line.startswith('data:'): data.append(line[5:].lstrip(' ')) else: raise ValueError('Expected JSON or SSE, not ' + content_type) matches = [value for value in values if isinstance(value, dict) and type(value.get('id')) is type(request_id) and value.get('id') == request_id] assert len(matches) == 1, 'missing or duplicate response ID' value = matches[0] assert value.get('jsonrpc') == '2.0' and 'error' not in value, 'JSON-RPC error' assert isinstance(value.get('result'), dict), 'missing result object' return value['result'] # Check our response gate against malformed/error responses before contacting Yoya. ok = {'jsonrpc': '2.0', 'id': 9, 'result': {'tools': []}} assert decode_rpc(json.dumps(ok), 'application/json', 9) == {'tools': []} assert decode_rpc('event: message\ndata: ' + json.dumps(ok) + '\n\n', 'text/event-stream', 9) == {'tools': []} for body, kind in [ ('not JSON', 'application/json'), (json.dumps({'jsonrpc': '2.0', 'id': 9, 'error': {'code': -32600}}), 'application/json'), (json.dumps({'jsonrpc': '2.0', 'id': 8, 'result': {}}), 'application/json'), ('data: not JSON\n\n', 'text/event-stream'), ('<html>unavailable</html>', 'text/html'), ]: try: decode_rpc(body, kind, 9) except (ValueError, AssertionError): continue raise AssertionError('Invalid response was accepted') print('Yoya response gate OK: JSON/SSE positives and five malformed/error cases rejected') with tempfile.TemporaryDirectory(prefix='yoya-discovery-') as directory: session = None def request(payload, request_id=None): assert payload['method'] in {'initialize', 'notifications/initialized', 'tools/list'} headers_path = pathlib.Path(directory) / 'headers' body_path = pathlib.Path(directory) / 'body' args = ['curl', '--disable', '--proto', '=https', '--tlsv1.2', '--silent', '--show-error', '--connect-timeout', '10', '--max-time', '25', '--max-filesize', '1048576', '--dump-header', str(headers_path), '--output', str(body_path), '--write-out', '%{http_code}', '-H', 'Content-Type: application/json', '-H', 'Accept: application/json, text/event-stream', '-H', 'MCP-Protocol-Version: ' + VERSION] if session: args.extend(['-H', 'Mcp-Session-Id: ' + session]) args.extend(['--data-binary', json.dumps(payload), ENDPOINT]) done = subprocess.run(args, capture_output=True, text=True, timeout=30) assert done.returncode == 0, 'HTTP transport failed: ' + done.stderr status = int(done.stdout) headers = {} for line in headers_path.read_text().splitlines(): if ':' in line: name, value = line.split(':', 1) headers[name.lower()] = value.strip() body = body_path.read_text() assert len(body.encode()) <= 1048576, 'response too large' if request_id is None: assert status == 202 and not body.strip(), 'initialized notification was not accepted' return {}, headers assert status == 200, 'HTTP ' + str(status) return decode_rpc(body, headers.get('content-type', ''), request_id), headers initialization, headers = request({'jsonrpc': '2.0', 'id': 1, 'method': 'initialize', 'params': { 'protocolVersion': VERSION, 'capabilities': {}, 'clientInfo': {'name': 'flowstacks-readonly-check', 'version': '1.0'}}}, 1) assert initialization.get('protocolVersion') == VERSION, 'unsupported negotiated version' assert isinstance(initialization.get('serverInfo'), dict) assert isinstance(initialization.get('capabilities', {}).get('tools'), dict) session = headers.get('mcp-session-id') if session: assert len(session) <= 1024 and all(0x21 <= ord(character) <= 0x7e for character in session) print('Yoya initialize OK: compatible protocol and advertised tools capability') request({'jsonrpc': '2.0', 'method': 'notifications/initialized'}) print('Yoya initialized OK: HTTP 202') discovered = [] cursor, seen_cursors = None, set() for page in range(5): params = {} if cursor is None else {'cursor': cursor} result, _ = request({'jsonrpc': '2.0', 'id': page + 2, 'method': 'tools/list', 'params': params}, page + 2) assert isinstance(result.get('tools'), list) discovered.extend(result['tools']) cursor = result.get('nextCursor') if cursor is None: break assert isinstance(cursor, str) and len(cursor) <= 4096 and cursor not in seen_cursors seen_cursors.add(cursor) else: raise AssertionError('tool pagination exceeded the bounded check') names = [tool.get('name') for tool in discovered] assert all(isinstance(name, str) and name for name in names) assert len(names) == len(set(names)), 'duplicate tool names' tools = dict(zip(names, discovered)) required = {'get_booking_page', 'list_available_times', 'preview_booking', 'book_meeting', 'get_booking'} assert required <= set(tools), 'required booking tools missing' assert all(tool.get('inputSchema', {}).get('type') == 'object' for tool in discovered) booking = tools['book_meeting']['inputSchema'] assert {'confirmed', 'confirmationToken'} <= set(booking.get('required', [])) assert booking['properties']['confirmed'].get('const') is True assert booking['properties']['confirmationToken'].get('type') == 'string' assert tools['preview_booking'].get('annotations', {}).get('readOnlyHint') is True assert tools['book_meeting'].get('annotations', {}).get('readOnlyHint') is False assert 'token' in tools['get_booking']['inputSchema'].get('required', []) print('Yoya discovery OK: five required tools and object input schemas') print('Yoya advertised confirmation contract OK: token and confirmed=true required; enforcement not tested') print('No tools/call, booking, cancellation, guest data, credentials, calendar sync, or email delivery used') PY - 2
Optionally connect Claude Code and inspect a controlled test page
In a test project, run: claude mcp add --transport http yoya-book https://yoya.ai/api/mcp/book. Open Claude Code there and inspect /mcp. This exposes write tools as well as read tools; never auto-approve all calls. Ask it to look up your own test booking page with get_booking_page, then use list_available_times with your explicit IANA time zone and limit 10. Do not book yet and never invent a slot ID. Public guest MCP needs no Yoya login and grants no host access.
- 3
Review a preview before any separately approved booking
Provide synthetic guest details with an inbox you control and select a returned free slot. Ask for preview_booking and stop to review the date, time zone, duration, meeting option, required fields and email. Only after explicit approval should the client call book_meeting with the preview fields, requestId and confirmationToken unchanged and confirmed=true. Reuse the same values when retrying that same approved request. Refresh and obtain new approval if the slot or details change. A PAYMENT_REQUIRED response is not a confirmed meeting and this recipe authorizes no payment. These manual steps have not been independently executed by FlowStacks.
- 4
Check each result separately and clean up only with approval
Inspect the host dashboard, booking management page, connected calendar and controlled inbox independently. A reserved slot does not prove calendar synchronization or email delivery; do not book again merely because email is delayed. Private manage tokens authorize individual bookings and must stay private. Host-side bookings and briefs use the separate OAuth endpoint https://yoya.ai/api/mcp with appropriate permissions, not public guest access. Review any generated brief against the input yourself. Explicitly approve cancellation of the test meeting afterward, since it can notify participants. None of these outcomes inherits the setup badge.
Eval, 7 fixtures
Last passed: verified todayresponse-gatecontainstimeout 180s · max $0Expected:
Yoya response gate OK: JSON/SSE positives and five malformed/error cases rejectedinitializecontainstimeout 180s · max $0Expected:
Yoya initialize OK: compatible protocol and advertised tools capabilityinitializedcontainstimeout 180s · max $0Expected:
Yoya initialized OK: HTTP 202tool-schemascontainstimeout 180s · max $0Expected:
Yoya discovery OK: five required tools and object input schemasadvertised-confirmationcontainstimeout 180s · max $0Expected:
Yoya advertised confirmation contract OK: token and confirmed=true required; enforcement not testedno-bookingcontainstimeout 180s · max $0Expected:
No tools/call, booking, cancellation, guest data, credentials, calendar sync, or email delivery usedclean-exitexit_codetimeout 180s · max $0Expected:
0
Results
CI rejects malformed/error JSON-RPC and SSE responses, performs credential-free initialize, initialized notification and tools/list requests against Yoya's fixed public endpoint, and checks the five required tools, object input schemas and advertised confirmation-token/confirmed=true fields. This is a live-network setup smoke check, not a fully offline fixture or end-to-end booking test. It does not call any tool, create or cancel a meeting, authenticate a host, inspect a real calendar or inbox, or grade a generated brief. Schema fields and tool annotations do not prove server enforcement or human consent.
Did this work for you?
Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.
Related workflows
- Invoice Chaser: calculate late fees and choose a reminder stage
- Mastra: stop on incomplete private soccer moneyline outcome sets
- STEM source → reviewable LaTeX notes with a sandboxed compile gate
- One AGENTS.md, no drift: prove CLAUDE.md is a real symlink and the generated files are in sync
- Self-hosting the open-source stack? Prove your backup actually restores before you need it
- Chat with a CSV, but pin a known-answer guardrail so a wrong query cannot pass
Liked this workflow?
Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).