AutomationCommercialFreeActiveMachine-verified· intermediate · ~15 min setup

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.

FlowStacks verification by Shilpa Mitra·Submitted by @leoqwe· verified today· v1.0.0

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 test

Side 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. 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. 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. 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. 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 today
  • response-gatecontainstimeout 180s · max $0

    Expected: Yoya response gate OK: JSON/SSE positives and five malformed/error cases rejected

  • initializecontainstimeout 180s · max $0

    Expected: Yoya initialize OK: compatible protocol and advertised tools capability

  • initializedcontainstimeout 180s · max $0

    Expected: Yoya initialized OK: HTTP 202

  • tool-schemascontainstimeout 180s · max $0

    Expected: Yoya discovery OK: five required tools and object input schemas

  • advertised-confirmationcontainstimeout 180s · max $0

    Expected: Yoya advertised confirmation contract OK: token and confirmed=true required; enforcement not tested

  • no-bookingcontainstimeout 180s · max $0

    Expected: No tools/call, booking, cancellation, guest data, credentials, calendar sync, or email delivery used

  • clean-exitexit_codetimeout 180s · max $0

    Expected: 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

Liked this workflow?

Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).