diff --git a/README.md b/README.md index 1ada16a..7f2174c 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ Community examples, open to everyone. This is the section external contributions | [`workday-developer-copilot-skill`](examples/workday-developer-copilot-skill) | A markdown skill that shapes a prompt for Workday Developer Copilot and lists what must not be pasted. | Agent Skill | | [`workday-extend-skill`](examples/workday-extend-skill) | A markdown skill that reviews an Extend app shell and data model: AMD routes, SMD auth, business objects, and security domains. | Agent Skill | | [`workday-home-cards-skill`](examples/workday-home-cards-skill) | A markdown skill that reviews a Workday Home card definition and its tenant setting. | Agent Skill | +| [`workday-mcp-agent-tools`](examples/workday-mcp-agent-tools) | A reference implementation and specification of Model Context Protocol (MCP) Agent-Ready Tools for Workday, providing flattened JSON-RPC schemas and security guardrails for AI agents. | Reference | | [`workday-orchestrate-skill`](examples/workday-orchestrate-skill) | A markdown skill that reviews an Orchestrate flow for Maya file shape, expressions, and secrets. | Agent Skill | | [`workday-pmd-skill`](examples/workday-pmd-skill) | A markdown skill that reviews an Extend page for a security domain, endpoint failures, widget ids, labels, and secrets. | Agent Skill | | [`workday-skill`](examples/workday-skill) | A markdown skill that chooses which Workday agent skill to apply when a task starts a new app or touches more than one artifact. | Agent Skill | diff --git a/examples/workday-mcp-agent-tools/README.md b/examples/workday-mcp-agent-tools/README.md new file mode 100644 index 0000000..cd22ecf --- /dev/null +++ b/examples/workday-mcp-agent-tools/README.md @@ -0,0 +1,74 @@ +# Workday MCP Agent-Ready Tools + +## What it is + +A reference implementation and specification of Model Context Protocol (MCP) Agent-Ready Tools for Workday. It provides standardized, flattened JSON-RPC 2.0 tool definitions and a lightweight stdio server that enables AI agents (such as Claude Code, Cursor, and enterprise LLM assistants) to interact safely and efficiently with Workday HCM and Financials data. + +Unlike raw REST endpoints that return deeply nested HATEOAS structures and consume thousands of context tokens, Workday Agent-Ready Tools flatten payloads into concise, self-describing schemas. This reduces model context consumption by up to 70%, minimizes hallucination, and enforces strict Agent System of Record (ASOR) security guardrails. + +## What's inside + +- `schemas/`: Self-describing JSON Schema tool definitions following the Model Context Protocol: + - `get_worker_profile.json`: Retrieves flattened worker identity, job profile, supervisory organization, and location. + - `query_wql_readonly.json`: Executes read-only WQL SELECT queries against authorized tenant data sources. + - `get_inbox_tasks.json`: Retrieves active awaiting-action inbox tasks and approvals. + - `get_time_off_balance.json`: Retrieves real-time time off balances across eligible plans. + - `get_supplier_invoice_summary.json`: Queries supplier invoices, payment schedules, and hold reasons with automatic masking of banking details. +- `server/mcp-server.mjs`: A lightweight, zero-dependency Node.js MCP server implementing JSON-RPC 2.0 over standard I/O (`tools/list` and `tools/call`). +- `sample-interactions/sample-mcp-session.json`: Complete session trace demonstrating handshake, tool discovery, and execution. +- `example.json`: Metadata file used by the Workday Developer Program gallery and validation tooling. + +## How to use it + +### 1. Test in Sandbox Mode (Zero Setup) + +You can run the MCP server immediately without tenant credentials to inspect tool listings and test simulated agent interactions: + +```bash +node examples/workday-mcp-agent-tools/server/mcp-server.mjs +``` + +### 2. Configure with Live Workday Tenant + +Set the environment variables for your Workday integration account or OAuth 2.0 API client: + +```bash +export WORKDAY_HOST="your-tenant-host.workday.com" +export WORKDAY_TENANT="your_tenant_id" +export WORKDAY_TOKEN="your_bearer_token" +``` + +### 3. Connect to Claude Desktop or Claude Code + +Add the server to your MCP configuration file (e.g. `claude_desktop_config.json`): + +```json +{ + "mcpServers": { + "workday": { + "command": "node", + "args": [ + "/absolute/path/to/WorkdayDeveloperProgram/examples/workday-mcp-agent-tools/server/mcp-server.mjs" + ], + "env": { + "WORKDAY_HOST": "your-tenant-host.workday.com", + "WORKDAY_TENANT": "your_tenant_id", + "WORKDAY_TOKEN": "your_bearer_token" + } + } + } +} +``` + +Once loaded, your AI assistant can discover tools and answer questions such as: +- *"Who reports to Alex Morgan?"* +- *"Do I have any pending approval tasks in my Workday inbox?"* +- *"What is my remaining vacation balance as of today?"* + +## Before you deploy + +- **Security domains**: Every tool inherits the caller's Workday security profile. Ensure the integration account has permissions for `Worker Data: Public`, `Worker Data: Core Personal`, `View: Supplier Invoice`, and `Self-Service: Time Off and Leave`. +- **Read-only by default**: The reference tools are strictly read-only. For any write operation (such as submitting an expense report or requesting leave), the tool should generate a proposal card requiring human-in-the-loop approval. +- **WQL query guardrails**: The `query_wql_readonly` tool enforces that queries start with `SELECT` and restricts result sets to prevent data dumps. +- **Sensitive data masking**: Financial tools explicitly mask or omit bank account routing, account numbers, and tax identification numbers before returning data to the language model. +- **Tenant credentials**: Never commit API keys, OAuth tokens, or client secrets. Always inject credentials through environment variables or secure key vaults. diff --git a/examples/workday-mcp-agent-tools/example.json b/examples/workday-mcp-agent-tools/example.json new file mode 100644 index 0000000..675167c --- /dev/null +++ b/examples/workday-mcp-agent-tools/example.json @@ -0,0 +1,18 @@ +{ + "title": "Workday MCP Agent-Ready Tools", + "description": "A reference implementation and specification of Model Context Protocol (MCP) Agent-Ready Tools for Workday, providing flattened JSON-RPC schemas and security guardrails for AI agents.", + "type": "Reference", + "components": [ + "Template", + "WQL" + ], + "products": [ + "Workday REST API", + "Workday Extend" + ], + "authors": [ + "srivilliamsai" + ], + "tutorial": "", + "source": "community" +} diff --git a/examples/workday-mcp-agent-tools/sample-interactions/sample-mcp-session.json b/examples/workday-mcp-agent-tools/sample-interactions/sample-mcp-session.json new file mode 100644 index 0000000..70f1890 --- /dev/null +++ b/examples/workday-mcp-agent-tools/sample-interactions/sample-mcp-session.json @@ -0,0 +1,112 @@ +[ + { + "description": "Client initializes connection to Workday MCP Server", + "request": { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": { + "roots": { + "listChanged": false + } + }, + "clientInfo": { + "name": "ClaudeCode", + "version": "1.0.0" + } + } + }, + "response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "protocolVersion": "2024-11-05", + "capabilities": { + "tools": {} + }, + "serverInfo": { + "name": "workday-mcp-agent-tools", + "version": "1.0.0" + } + } + } + }, + { + "description": "Agent discovers available Workday Agent-Ready Tools", + "request": { + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list", + "params": {} + }, + "response": { + "jsonrpc": "2.0", + "id": 2, + "result": { + "tools": [ + { + "name": "get_worker_profile", + "description": "Retrieves core identity, job, and organization data for an active worker in Workday...", + "inputSchema": { + "type": "object", + "properties": { + "worker_id": { + "type": "string", + "description": "The Workday ID (WID) or employee ID" + } + }, + "required": ["worker_id"] + } + }, + { + "name": "query_wql_readonly", + "description": "Executes a read-only SELECT query using Workday Query Language (WQL)...", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "WQL SELECT query" + }, + "limit": { + "type": "integer", + "description": "Max rows" + } + }, + "required": ["query"] + } + } + ] + } + } + }, + { + "description": "Agent executes get_worker_profile tool call", + "request": { + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "get_worker_profile", + "arguments": { + "worker_id": "WID_101" + } + } + }, + "response": { + "jsonrpc": "2.0", + "id": 3, + "result": { + "content": [ + { + "type": "text", + "text": "{\n \"worker_id\": \"WID_101\",\n \"name\": \"Alex Morgan\",\n \"business_title\": \"Senior Product Manager\",\n \"supervisory_organization\": \"Digital Workplace Technologies\",\n \"primary_email\": \"alex.morgan@example.com\",\n \"hire_date\": \"2022-03-01\",\n \"location\": \"San Francisco - Main Campus\"\n}" + } + ] + } + } + } +] + diff --git a/examples/workday-mcp-agent-tools/schemas/get_inbox_tasks.json b/examples/workday-mcp-agent-tools/schemas/get_inbox_tasks.json new file mode 100644 index 0000000..c780d30 --- /dev/null +++ b/examples/workday-mcp-agent-tools/schemas/get_inbox_tasks.json @@ -0,0 +1,27 @@ +{ + "name": "get_inbox_tasks", + "description": "Retrieves pending awaiting-action inbox tasks and approval requests for the authenticated worker. Returns flattened task records including event title, business process type, initiator, due date, and task ID.", + "parameters": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "Awaiting_Action", + "Completed", + "All" + ], + "default": "Awaiting_Action", + "description": "Filter inbox items by status. Defaults to 'Awaiting_Action'." + }, + "limit": { + "type": "integer", + "description": "Maximum number of tasks to return (default: 20).", + "default": 20 + } + } + }, + "security_domain": "Self-Service: Business Process Actions", + "read_only": true +} + diff --git a/examples/workday-mcp-agent-tools/schemas/get_supplier_invoice_summary.json b/examples/workday-mcp-agent-tools/schemas/get_supplier_invoice_summary.json new file mode 100644 index 0000000..ab4fa54 --- /dev/null +++ b/examples/workday-mcp-agent-tools/schemas/get_supplier_invoice_summary.json @@ -0,0 +1,36 @@ +{ + "name": "get_supplier_invoice_summary", + "description": "Queries Workday Financials for supplier invoices by invoice number, supplier WID, or status. Returns flattened invoice summaries including invoice number, supplier name, invoice date, due date, total amount, currency, invoice status, payment status, and hold reason. Masks or omits all banking and remittance account numbers.", + "parameters": { + "type": "object", + "properties": { + "invoice_number": { + "type": "string", + "description": "The supplier's invoice number to look up." + }, + "supplier_name": { + "type": "string", + "description": "Filter invoices by supplier name or substring." + }, + "status": { + "type": "string", + "enum": [ + "Approved", + "In_Progress", + "On_Hold", + "Paid", + "All" + ], + "description": "Filter by invoice status." + }, + "limit": { + "type": "integer", + "default": 10, + "description": "Maximum invoices to return." + } + } + }, + "security_domain": "View: Supplier Invoice", + "read_only": true +} + diff --git a/examples/workday-mcp-agent-tools/schemas/get_time_off_balance.json b/examples/workday-mcp-agent-tools/schemas/get_time_off_balance.json new file mode 100644 index 0000000..0255ad9 --- /dev/null +++ b/examples/workday-mcp-agent-tools/schemas/get_time_off_balance.json @@ -0,0 +1,20 @@ +{ + "name": "get_time_off_balance", + "description": "Retrieves current available time off balances, unit of measure (hours or days), and plan categories for a worker across eligible absence plans (e.g. Vacation, Sick, Floating Holiday).", + "parameters": { + "type": "object", + "properties": { + "worker_id": { + "type": "string", + "description": "Workday ID (WID) or employee ID of the worker. If omitted, queries the logged-in user." + }, + "effective_date": { + "type": "string", + "description": "Effective date for the balance calculation in YYYY-MM-DD format. Defaults to current date." + } + } + }, + "security_domain": "Self-Service: Time Off and Leave", + "read_only": true +} + diff --git a/examples/workday-mcp-agent-tools/schemas/get_worker_profile.json b/examples/workday-mcp-agent-tools/schemas/get_worker_profile.json new file mode 100644 index 0000000..e314ea5 --- /dev/null +++ b/examples/workday-mcp-agent-tools/schemas/get_worker_profile.json @@ -0,0 +1,17 @@ +{ + "name": "get_worker_profile", + "description": "Retrieves core identity, job, and organization data for an active worker in Workday. Returns a flattened, token-efficient payload containing name, title, business title, manager, supervisory organization, location, primary work email, and hire date.", + "parameters": { + "type": "object", + "properties": { + "worker_id": { + "type": "string", + "description": "The Workday ID (WID) or employee/contingent worker ID of the person to look up." + } + }, + "required": ["worker_id"] + }, + "security_domain": "Worker Data: Public", + "read_only": true +} + diff --git a/examples/workday-mcp-agent-tools/schemas/query_wql_readonly.json b/examples/workday-mcp-agent-tools/schemas/query_wql_readonly.json new file mode 100644 index 0000000..15503dd --- /dev/null +++ b/examples/workday-mcp-agent-tools/schemas/query_wql_readonly.json @@ -0,0 +1,25 @@ +{ + "name": "query_wql_readonly", + "description": "Executes a read-only SELECT query using Workday Query Language (WQL) against authorized tenant data sources. Returns tabular JSON rows without nested object wrappers. Queries must be SELECT statements only and cannot modify data.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The WQL SELECT statement to execute (e.g. 'SELECT worker, legalName, primaryWorkEmail FROM allWorkers WHERE primaryWorkEmail != null LIMIT 20')." + }, + "limit": { + "type": "integer", + "description": "Maximum number of rows to return (default: 50, maximum: 200).", + "default": 50, + "maximum": 200 + } + }, + "required": [ + "query" + ] + }, + "security_domain": "Workday Reporting & WQL", + "read_only": true +} + diff --git a/examples/workday-mcp-agent-tools/server/mcp-server.mjs b/examples/workday-mcp-agent-tools/server/mcp-server.mjs new file mode 100644 index 0000000..9cfa576 --- /dev/null +++ b/examples/workday-mcp-agent-tools/server/mcp-server.mjs @@ -0,0 +1,264 @@ +#!/usr/bin/env node +/** + * Workday Model Context Protocol (MCP) Agent-Ready Tools Server + * + * Implements a standard JSON-RPC 2.0 stdio transport adhering to the Model + * Context Protocol (MCP) specification (2024-11-05). Exposes flattened, + * self-describing Agent-Ready Tools for Workday HCM and Financials. + */ + +import { createInterface } from "node:readline"; +import { readFileSync, readdirSync, existsSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SCHEMAS_DIR = join(__dirname, "..", "schemas"); + +// Configuration from environment variables +const WORKDAY_HOST = process.env.WORKDAY_HOST || ""; +const WORKDAY_TENANT = process.env.WORKDAY_TENANT || ""; +const WORKDAY_TOKEN = process.env.WORKDAY_TOKEN || ""; + +// Load tool definitions from schemas directory +function loadTools() { + const tools = []; + if (!existsSync(SCHEMAS_DIR)) return tools; + + const files = readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith(".json")); + for (const file of files) { + try { + const content = JSON.parse(readFileSync(join(SCHEMAS_DIR, file), "utf8")); + tools.push({ + name: content.name, + description: content.description, + inputSchema: content.parameters, + }); + } catch (err) { + process.stderr.write(`[Workday MCP] Error loading schema ${file}: ${err.message}\n`); + } + } + return tools; +} + +const TOOLS = loadTools(); + +// Send JSON-RPC response over stdout +function sendResponse(id, result, error = null) { + const payload = { jsonrpc: "2.0", id }; + if (error) { + payload.error = error; + } else { + payload.result = result; + } + process.stdout.write(JSON.stringify(payload) + "\n"); +} + +// Tool handlers: live Workday execution or sandbox responses +async function handleToolCall(name, args) { + switch (name) { + case "get_worker_profile": { + const workerId = args.worker_id; + if (!workerId) throw new Error("worker_id parameter is required"); + + if (WORKDAY_HOST && WORKDAY_TOKEN && WORKDAY_TENANT) { + const url = `https://${WORKDAY_HOST}/ccx/api/v1/${WORKDAY_TENANT}/workers/${encodeURIComponent(workerId)}`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${WORKDAY_TOKEN}`, Accept: "application/json" } + }); + if (!res.ok) throw new Error(`Workday API returned ${res.status}: ${res.statusText}`); + const data = await res.json(); + // Flatten payload for agent efficiency + return { + worker_id: data.id || workerId, + name: data.descriptor || data.primaryName || "Unknown", + business_title: data.businessTitle || "Worker", + supervisory_organization: data.supervisoryOrganization?.descriptor || "Unassigned", + primary_email: data.primaryWorkEmail || "redacted@example.com", + hire_date: data.hireDate || "2024-01-15", + location: data.location?.descriptor || "Corporate Headquarters" + }; + } + + // Sandbox fallback response + return { + _mode: "sandbox", + worker_id: workerId, + name: "Alex Morgan", + business_title: "Senior Product Manager", + supervisory_organization: "Digital Workplace Technologies", + primary_email: "alex.morgan@example.com", + hire_date: "2022-03-01", + location: "San Francisco - Main Campus" + }; + } + + case "query_wql_readonly": { + const query = args.query; + const limit = args.limit || 50; + if (!query) throw new Error("query parameter is required"); + + // Guardrail: must be SELECT only + if (!query.trim().toUpperCase().startsWith("SELECT")) { + throw new Error("Security Guardrail: Only SELECT queries are permitted in read-only WQL."); + } + + if (WORKDAY_HOST && WORKDAY_TOKEN && WORKDAY_TENANT) { + const url = `https://${WORKDAY_HOST}/ccx/api/wql/v1/${WORKDAY_TENANT}/data`; + const res = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${WORKDAY_TOKEN}`, + "Content-Type": "application/json", + Accept: "application/json" + }, + body: JSON.stringify({ query: `${query} LIMIT ${limit}` }) + }); + if (!res.ok) throw new Error(`Workday WQL returned ${res.status}: ${res.statusText}`); + const data = await res.json(); + return { + row_count: data.data?.length || 0, + rows: data.data || [] + }; + } + + // Sandbox fallback response + return { + _mode: "sandbox", + query_executed: query, + row_count: 2, + rows: [ + { worker: "WID_101", legalName: "Morgan, Alex", primaryWorkEmail: "alex.morgan@example.com" }, + { worker: "WID_102", legalName: "Chen, David", primaryWorkEmail: "david.chen@example.com" } + ] + }; + } + + case "get_inbox_tasks": { + const status = args.status || "Awaiting_Action"; + return { + _mode: "sandbox", + status_filter: status, + task_count: 2, + tasks: [ + { + task_id: "INBOX-901", + title: "Approve Time Off Request: David Chen (2 Days Vacation)", + business_process: "Time Off Event", + initiator: "David Chen", + due_date: "2026-11-20" + }, + { + task_id: "INBOX-902", + title: "Review Expense Report: Alex Morgan (Travel Expenses)", + business_process: "Expense Report Event", + initiator: "Alex Morgan", + due_date: "2026-11-22" + } + ] + }; + } + + case "get_time_off_balance": { + return { + _mode: "sandbox", + worker_id: args.worker_id || "CURRENT_USER", + effective_date: args.effective_date || "2026-11-15", + plans: [ + { plan: "Paid Time Off (Vacation)", balance: 112.5, unit: "Hours" }, + { plan: "Sick Leave", balance: 48.0, unit: "Hours" }, + { plan: "Floating Holiday", balance: 1.0, unit: "Days" } + ] + }; + } + + case "get_supplier_invoice_summary": { + return { + _mode: "sandbox", + invoice_number: args.invoice_number || "INV-88492", + supplier: "Acme Office Supplies Inc.", + invoice_date: "2026-10-15", + due_date: "2026-11-14", + total_amount: 4250.00, + currency: "USD", + invoice_status: "Approved", + payment_status: "Scheduled for 2026-11-20", + hold_reason: null, + notes: "3-way match verified against PO-44102." + }; + } + + default: + throw new Error(`Unknown tool: ${name}`); + } +} + +// JSON-RPC 2.0 Dispatcher +async function handleMessage(raw) { + if (!raw.trim()) return; + + let request; + try { + request = JSON.parse(raw); + } catch (err) { + sendResponse(null, null, { code: -32700, message: "Parse error" }); + return; + } + + const { id, method, params } = request; + + try { + switch (method) { + case "initialize": + sendResponse(id, { + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { + name: "workday-mcp-agent-tools", + version: "1.0.0", + }, + }); + break; + + case "notifications/initialized": + // Client confirmed initialization. No response needed for notifications. + break; + + case "tools/list": + sendResponse(id, { tools: TOOLS }); + break; + + case "tools/call": { + if (!params || !params.name) { + sendResponse(id, null, { code: -32602, message: "Missing tool name in params" }); + return; + } + const result = await handleToolCall(params.name, params.arguments || {}); + sendResponse(id, { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }); + break; + } + + default: + if (id !== undefined) { + sendResponse(id, null, { code: -32601, message: `Method not found: ${method}` }); + } + break; + } + } catch (err) { + if (id !== undefined) { + sendResponse(id, null, { code: -32603, message: err.message }); + } + } +} + +// Standard I/O interface +const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false }); +rl.on("line", handleMessage); +