@mindstone-engineering/mcp-server-workday 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Workday authentication module.
3
+ *
4
+ * OAuth2 dual grant type: client_credentials (default) + refresh_token (when available).
5
+ * Credentials managed via env vars or configured at runtime.
6
+ *
7
+ * Environment variables:
8
+ * - WORKDAY_HOST: Workday API domain (e.g., wd5-impl-services1.workday.com)
9
+ * - WORKDAY_TENANT: Customer's Workday tenant name
10
+ * - WORKDAY_CLIENT_ID: OAuth client ID
11
+ * - WORKDAY_CLIENT_SECRET: OAuth client secret
12
+ * - WORKDAY_REFRESH_TOKEN: Optional refresh token (enables refresh_token grant)
13
+ */
14
+ export declare function getHost(): string;
15
+ export declare function setHost(h: string): void;
16
+ export declare function getTenant(): string;
17
+ export declare function setTenant(t: string): void;
18
+ export declare function getClientId(): string;
19
+ export declare function setClientId(id: string): void;
20
+ export declare function getClientSecret(): string;
21
+ export declare function setClientSecret(s: string): void;
22
+ export declare function getRefreshToken(): string;
23
+ export declare function setRefreshToken(t: string): void;
24
+ export declare function clearTokenCache(): void;
25
+ export declare function isConfigured(): boolean;
26
+ export declare function getTokenUrl(): string;
27
+ export declare function getApiBaseUrl(): string;
28
+ export declare function validateHost(rawHost: string): {
29
+ valid: boolean;
30
+ host?: string;
31
+ error?: string;
32
+ };
33
+ export declare function getAccessToken(): Promise<string>;
34
+ //# sourceMappingURL=auth.d.ts.map
package/dist/auth.js ADDED
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Workday authentication module.
3
+ *
4
+ * OAuth2 dual grant type: client_credentials (default) + refresh_token (when available).
5
+ * Credentials managed via env vars or configured at runtime.
6
+ *
7
+ * Environment variables:
8
+ * - WORKDAY_HOST: Workday API domain (e.g., wd5-impl-services1.workday.com)
9
+ * - WORKDAY_TENANT: Customer's Workday tenant name
10
+ * - WORKDAY_CLIENT_ID: OAuth client ID
11
+ * - WORKDAY_CLIENT_SECRET: OAuth client secret
12
+ * - WORKDAY_REFRESH_TOKEN: Optional refresh token (enables refresh_token grant)
13
+ */
14
+ import { WorkdayError, USER_AGENT, REQUEST_TIMEOUT_MS } from './types.js';
15
+ import { bridgeRequest } from './bridge.js';
16
+ // ── Runtime credentials ──
17
+ let workdayHost = '';
18
+ let workdayTenant = process.env.WORKDAY_TENANT ?? '';
19
+ let clientId = process.env.WORKDAY_CLIENT_ID ?? '';
20
+ let clientSecret = process.env.WORKDAY_CLIENT_SECRET ?? '';
21
+ let refreshToken = process.env.WORKDAY_REFRESH_TOKEN ?? '';
22
+ // Validate WORKDAY_HOST from env at startup — reject private/localhost hosts
23
+ const _envHost = process.env.WORKDAY_HOST ?? '';
24
+ if (_envHost) {
25
+ const _hostResult = validateHost(_envHost);
26
+ if (_hostResult.valid) {
27
+ workdayHost = _hostResult.host;
28
+ }
29
+ else {
30
+ console.error(`[Workday] Ignoring invalid WORKDAY_HOST from env: ${_hostResult.error}`);
31
+ }
32
+ }
33
+ // ── Token cache ──
34
+ let cachedAccessToken = null;
35
+ let tokenExpiresAt = 0;
36
+ // ── Getters / setters ──
37
+ export function getHost() {
38
+ return workdayHost;
39
+ }
40
+ export function setHost(h) {
41
+ workdayHost = h;
42
+ }
43
+ export function getTenant() {
44
+ return workdayTenant;
45
+ }
46
+ export function setTenant(t) {
47
+ workdayTenant = t;
48
+ }
49
+ export function getClientId() {
50
+ return clientId;
51
+ }
52
+ export function setClientId(id) {
53
+ clientId = id;
54
+ }
55
+ export function getClientSecret() {
56
+ return clientSecret;
57
+ }
58
+ export function setClientSecret(s) {
59
+ clientSecret = s;
60
+ }
61
+ export function getRefreshToken() {
62
+ return refreshToken;
63
+ }
64
+ export function setRefreshToken(t) {
65
+ refreshToken = t;
66
+ }
67
+ export function clearTokenCache() {
68
+ cachedAccessToken = null;
69
+ tokenExpiresAt = 0;
70
+ }
71
+ export function isConfigured() {
72
+ return !!(workdayHost && workdayTenant && clientId && clientSecret);
73
+ }
74
+ export function getTokenUrl() {
75
+ return `https://${workdayHost}/ccx/oauth2/${workdayTenant}/token`;
76
+ }
77
+ export function getApiBaseUrl() {
78
+ return `https://${workdayHost}/ccx/api/v1/${workdayTenant}`;
79
+ }
80
+ // ── SSRF / Host validation ──
81
+ function normalizeHost(raw) {
82
+ let host = raw.trim();
83
+ host = host.replace(/^https?:\/\//i, '');
84
+ host = host.replace(/\/+$/, '');
85
+ return host;
86
+ }
87
+ function isPrivateOrLocalhost(host) {
88
+ const lower = host.toLowerCase();
89
+ if (lower === 'localhost' || lower === '[::1]') {
90
+ return true;
91
+ }
92
+ const ipMatch = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
93
+ if (ipMatch) {
94
+ const [, a, b] = ipMatch.map(Number);
95
+ if (a === 127)
96
+ return true;
97
+ if (a === 10)
98
+ return true;
99
+ if (a === 172 && b >= 16 && b <= 31)
100
+ return true;
101
+ if (a === 192 && b === 168)
102
+ return true;
103
+ if (a === 169 && b === 254)
104
+ return true;
105
+ if (a === 0)
106
+ return true;
107
+ }
108
+ return false;
109
+ }
110
+ export function validateHost(rawHost) {
111
+ const host = normalizeHost(rawHost);
112
+ if (!host || host.length === 0) {
113
+ return { valid: false, error: 'Host is required.' };
114
+ }
115
+ if (isPrivateOrLocalhost(host)) {
116
+ return { valid: false, error: 'Host must not be localhost or a private IP address.' };
117
+ }
118
+ if (host.length < 2 || !/^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$/.test(host)) {
119
+ return { valid: false, error: 'Host must be a valid hostname.' };
120
+ }
121
+ return { valid: true, host };
122
+ }
123
+ // ── Token exchange ──
124
+ export async function getAccessToken() {
125
+ if (!clientId || !clientSecret) {
126
+ throw new WorkdayError('Workday not configured. Call configure_workday_credentials first.', 'NOT_CONFIGURED', 'Configure Workday with your OAuth credentials first.');
127
+ }
128
+ if (cachedAccessToken && Date.now() < tokenExpiresAt) {
129
+ return cachedAccessToken;
130
+ }
131
+ const authHeader = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
132
+ const bodyParams = refreshToken
133
+ ? { grant_type: 'refresh_token', refresh_token: refreshToken }
134
+ : { grant_type: 'client_credentials' };
135
+ const body = new URLSearchParams(bodyParams);
136
+ let response;
137
+ try {
138
+ response = await fetch(getTokenUrl(), {
139
+ method: 'POST',
140
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
141
+ headers: {
142
+ Authorization: authHeader,
143
+ 'Content-Type': 'application/x-www-form-urlencoded',
144
+ Accept: 'application/json',
145
+ 'User-Agent': USER_AGENT,
146
+ },
147
+ body: body.toString(),
148
+ });
149
+ }
150
+ catch (error) {
151
+ if (error instanceof Error && error.name === 'TimeoutError') {
152
+ throw new WorkdayError('OAuth token request timed out', 'TIMEOUT', 'The request took too long. Check your Workday host and network connectivity.');
153
+ }
154
+ throw error;
155
+ }
156
+ if (!response.ok) {
157
+ let errorText;
158
+ try {
159
+ const errorBody = await response.json();
160
+ errorText = errorBody?.error_description || errorBody?.error || JSON.stringify(errorBody);
161
+ }
162
+ catch {
163
+ errorText = await response.text().catch(() => 'Unknown error');
164
+ }
165
+ throw new WorkdayError(`OAuth token exchange failed (${response.status}): ${errorText}`, 'AUTH_FAILED', 'Re-configure with configure_workday_credentials. Check client ID, secret, and tenant.');
166
+ }
167
+ const tokenData = await response.json();
168
+ cachedAccessToken = tokenData.access_token;
169
+ tokenExpiresAt = Date.now() + (tokenData.expires_in - 60) * 1000;
170
+ // Handle refresh token rotation
171
+ if (tokenData.refresh_token && tokenData.refresh_token !== refreshToken) {
172
+ refreshToken = tokenData.refresh_token;
173
+ bridgeRequest('/bundled/workday/update-refresh-token', {
174
+ refreshToken: tokenData.refresh_token,
175
+ }).catch((err) => {
176
+ console.error('Failed to persist rotated refresh token:', err instanceof Error ? err.message : String(err));
177
+ });
178
+ }
179
+ return cachedAccessToken;
180
+ }
181
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Path to bridge state file, supporting both current and legacy env vars.
3
+ */
4
+ export declare const BRIDGE_STATE_PATH: string;
5
+ /**
6
+ * Send a request to the host app bridge.
7
+ *
8
+ * The bridge is an HTTP server running inside the host app (e.g. Rebel)
9
+ * that handles credential management and other cross-process operations.
10
+ */
11
+ export declare const bridgeRequest: (urlPath: string, body: Record<string, unknown>) => Promise<{
12
+ success: boolean;
13
+ warning?: string;
14
+ error?: string;
15
+ }>;
16
+ //# sourceMappingURL=bridge.d.ts.map
package/dist/bridge.js ADDED
@@ -0,0 +1,43 @@
1
+ import * as fs from 'fs';
2
+ import { REQUEST_TIMEOUT_MS } from './types.js';
3
+ /**
4
+ * Path to bridge state file, supporting both current and legacy env vars.
5
+ */
6
+ export const BRIDGE_STATE_PATH = process.env.MCP_HOST_BRIDGE_STATE || process.env.MINDSTONE_REBEL_BRIDGE_STATE || '';
7
+ const loadBridgeState = () => {
8
+ if (!BRIDGE_STATE_PATH)
9
+ return null;
10
+ try {
11
+ const raw = fs.readFileSync(BRIDGE_STATE_PATH, 'utf8');
12
+ return JSON.parse(raw);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ };
18
+ /**
19
+ * Send a request to the host app bridge.
20
+ *
21
+ * The bridge is an HTTP server running inside the host app (e.g. Rebel)
22
+ * that handles credential management and other cross-process operations.
23
+ */
24
+ export const bridgeRequest = async (urlPath, body) => {
25
+ const bridge = loadBridgeState();
26
+ if (!bridge) {
27
+ return { success: false, error: 'Bridge not available' };
28
+ }
29
+ const response = await fetch(`http://127.0.0.1:${bridge.port}${urlPath}`, {
30
+ method: 'POST',
31
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ Authorization: `Bearer ${bridge.token}`,
35
+ },
36
+ body: JSON.stringify(body),
37
+ });
38
+ if (response.status === 401 || response.status === 403) {
39
+ return { success: false, error: `Bridge returned ${response.status}: unauthorized. Check host app authentication.` };
40
+ }
41
+ return response.json();
42
+ };
43
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Workday API HTTP client.
3
+ *
4
+ * Centralises Bearer auth injection, error handling,
5
+ * rate-limit retry with exponential backoff, and timeout handling.
6
+ *
7
+ * Auth: OAuth2 Bearer token via getAccessToken()
8
+ * Base URL: https://{host}/ccx/api/v1/{tenant}
9
+ */
10
+ /**
11
+ * Make an authenticated JSON request to the Workday REST API.
12
+ */
13
+ export declare function workdayFetch<T>(resourcePath: string, options?: RequestInit, retryCount?: number): Promise<T>;
14
+ //# sourceMappingURL=client.d.ts.map
package/dist/client.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Workday API HTTP client.
3
+ *
4
+ * Centralises Bearer auth injection, error handling,
5
+ * rate-limit retry with exponential backoff, and timeout handling.
6
+ *
7
+ * Auth: OAuth2 Bearer token via getAccessToken()
8
+ * Base URL: https://{host}/ccx/api/v1/{tenant}
9
+ */
10
+ import { WorkdayError, USER_AGENT, REQUEST_TIMEOUT_MS } from './types.js';
11
+ import { getAccessToken, getApiBaseUrl, isConfigured, clearTokenCache } from './auth.js';
12
+ /**
13
+ * Make an authenticated JSON request to the Workday REST API.
14
+ */
15
+ export async function workdayFetch(resourcePath, options = {}, retryCount = 0) {
16
+ if (!isConfigured()) {
17
+ throw new WorkdayError('Workday not configured. Call configure_workday_credentials first.', 'NOT_CONFIGURED', 'Configure Workday with your OAuth credentials first.');
18
+ }
19
+ const accessToken = await getAccessToken();
20
+ const url = `${getApiBaseUrl()}${resourcePath}`;
21
+ const headers = {
22
+ Authorization: `Bearer ${accessToken}`,
23
+ Accept: 'application/json',
24
+ 'User-Agent': USER_AGENT,
25
+ ...options.headers,
26
+ };
27
+ console.error(`[Workday API] ${options.method || 'GET'} ${url}`);
28
+ let response;
29
+ try {
30
+ response = await fetch(url, {
31
+ ...options,
32
+ signal: options.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
33
+ headers,
34
+ });
35
+ }
36
+ catch (error) {
37
+ if (error instanceof Error && error.name === 'TimeoutError') {
38
+ throw new WorkdayError('Request to Workday API timed out', 'TIMEOUT', 'The request took too long. Try again or check if the Workday API is available.');
39
+ }
40
+ throw error;
41
+ }
42
+ if (!response.ok) {
43
+ // Handle 429 with exponential backoff (max 3 retries)
44
+ if (response.status === 429 && retryCount < 3) {
45
+ const retryAfter = response.headers.get('Retry-After');
46
+ const waitMs = retryAfter
47
+ ? parseInt(retryAfter, 10) * 1000
48
+ : Math.min(1000 * Math.pow(2, retryCount), 8000);
49
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
50
+ return workdayFetch(resourcePath, options, retryCount + 1);
51
+ }
52
+ let errorText;
53
+ try {
54
+ const errorBody = await response.json();
55
+ const firstError = errorBody?.errors?.[0];
56
+ errorText = firstError?.message || firstError?.error || errorBody?.error || JSON.stringify(errorBody);
57
+ }
58
+ catch {
59
+ errorText = await response.text().catch(() => 'Unknown error');
60
+ }
61
+ if (response.status === 401) {
62
+ clearTokenCache();
63
+ throw new WorkdayError(`Authentication failed (${response.status}): ${errorText}`, 'AUTH_FAILED', 'Re-configure with configure_workday_credentials. Check client ID, secret, and tenant.');
64
+ }
65
+ if (response.status === 403) {
66
+ throw new WorkdayError(`Insufficient permissions (${response.status}): ${errorText}`, 'FORBIDDEN', 'Check ISU security group and domain permissions in Workday.');
67
+ }
68
+ if (response.status === 404) {
69
+ throw new WorkdayError(`Resource not found (${response.status}): ${errorText}`, 'NOT_FOUND', 'Verify the ID is correct.');
70
+ }
71
+ if (response.status === 429) {
72
+ throw new WorkdayError('Rate limited. Maximum retries exhausted.', 'RATE_LIMITED', 'Please wait before retrying.');
73
+ }
74
+ if (response.status >= 500) {
75
+ throw new WorkdayError(`Workday server error (${response.status}): ${errorText}`, 'SERVER_ERROR', 'Workday server error. Try again later.');
76
+ }
77
+ throw new WorkdayError(`Workday API error (${response.status}): ${errorText}`, `HTTP_${response.status}`, 'Check the request parameters and try again.');
78
+ }
79
+ return await response.json();
80
+ }
81
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Workday MCP Server
4
+ *
5
+ * Provides Workday HCM integration via Model Context Protocol.
6
+ * Read-only v1: workers (employees + contingent workers) and organizations.
7
+ *
8
+ * Uses the Workday REST API v1 directly via fetch().
9
+ * OAuth 2.0 token management with dual grant type support
10
+ * (client_credentials + refresh_token).
11
+ *
12
+ * Environment variables:
13
+ * - WORKDAY_HOST: Workday API domain (e.g., wd5-impl-services1.workday.com)
14
+ * - WORKDAY_TENANT: Customer's Workday tenant name
15
+ * - WORKDAY_CLIENT_ID: OAuth client ID
16
+ * - WORKDAY_CLIENT_SECRET: OAuth client secret
17
+ * - WORKDAY_REFRESH_TOKEN: Optional refresh token (enables refresh_token grant)
18
+ * - MCP_HOST_BRIDGE_STATE: Path to bridge state file for app communication
19
+ * - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path
20
+ */
21
+ export {};
22
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Workday MCP Server
4
+ *
5
+ * Provides Workday HCM integration via Model Context Protocol.
6
+ * Read-only v1: workers (employees + contingent workers) and organizations.
7
+ *
8
+ * Uses the Workday REST API v1 directly via fetch().
9
+ * OAuth 2.0 token management with dual grant type support
10
+ * (client_credentials + refresh_token).
11
+ *
12
+ * Environment variables:
13
+ * - WORKDAY_HOST: Workday API domain (e.g., wd5-impl-services1.workday.com)
14
+ * - WORKDAY_TENANT: Customer's Workday tenant name
15
+ * - WORKDAY_CLIENT_ID: OAuth client ID
16
+ * - WORKDAY_CLIENT_SECRET: OAuth client secret
17
+ * - WORKDAY_REFRESH_TOKEN: Optional refresh token (enables refresh_token grant)
18
+ * - MCP_HOST_BRIDGE_STATE: Path to bridge state file for app communication
19
+ * - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path
20
+ */
21
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
22
+ import { createServer } from './server.js';
23
+ async function main() {
24
+ const server = createServer();
25
+ const transport = new StdioServerTransport();
26
+ await server.connect(transport);
27
+ console.error('Workday MCP server running on stdio');
28
+ }
29
+ main().catch((error) => {
30
+ console.error('Fatal error:', error);
31
+ process.exit(1);
32
+ });
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function createServer(): McpServer;
3
+ //# sourceMappingURL=server.d.ts.map
package/dist/server.js ADDED
@@ -0,0 +1,13 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerConfigureTools, registerWorkerTools, registerOrganizationTools, } from './tools/index.js';
3
+ export function createServer() {
4
+ const server = new McpServer({
5
+ name: 'workday-mcp-server',
6
+ version: '0.1.0',
7
+ });
8
+ registerConfigureTools(server);
9
+ registerWorkerTools(server);
10
+ registerOrganizationTools(server);
11
+ return server;
12
+ }
13
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Workday credential configuration tool.
3
+ *
4
+ * Validates host (SSRF prevention), attempts token exchange + API probe,
5
+ * persists via bridge, and updates runtime credentials.
6
+ */
7
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ export declare function registerConfigureTools(server: McpServer): void;
9
+ //# sourceMappingURL=configure.d.ts.map
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Workday credential configuration tool.
3
+ *
4
+ * Validates host (SSRF prevention), attempts token exchange + API probe,
5
+ * persists via bridge, and updates runtime credentials.
6
+ */
7
+ import { z } from 'zod';
8
+ import { WorkdayError, USER_AGENT, REQUEST_TIMEOUT_MS } from '../types.js';
9
+ import { withErrorHandling } from '../utils.js';
10
+ import { validateHost, setHost, setTenant, setClientId, setClientSecret, setRefreshToken, clearTokenCache, } from '../auth.js';
11
+ import { bridgeRequest } from '../bridge.js';
12
+ export function registerConfigureTools(server) {
13
+ server.registerTool('configure_workday_credentials', {
14
+ description: `Configure Workday API credentials. Call this when the user provides their Workday OAuth credentials.
15
+
16
+ SETUP PREREQUISITES:
17
+ 1. A Workday Integration System User (ISU) with appropriate security group access
18
+ 2. An API Client registered in Workday (Tenant Setup > API Clients)
19
+ 3. The Client ID and Client Secret from the API Client registration
20
+ 4. Optionally, a pre-generated Refresh Token (from OAuth token exchange)
21
+
22
+ PARAMETERS:
23
+ - host: Workday API domain (e.g., "wd5-impl-services1.workday.com")
24
+ - tenant: Your Workday tenant name (e.g., "acme_corp")
25
+ - client_id: OAuth Client ID from API Client registration
26
+ - client_secret: OAuth Client Secret
27
+ - refresh_token: (Optional) Pre-generated refresh token. If omitted, uses client_credentials grant.
28
+
29
+ COMMON MISTAKES:
30
+ - Host should be just the domain (e.g., "wd5-impl-services1.workday.com"), not a full URL
31
+ - Tenant name is case-sensitive
32
+ - The ISU must have permissions for the REST API resources you want to access`,
33
+ inputSchema: z.object({
34
+ host: z.string().describe('Workday API domain (e.g., "wd5-impl-services1.workday.com")'),
35
+ tenant: z.string().describe('Workday tenant name (e.g., "acme_corp")'),
36
+ client_id: z.string().describe('OAuth Client ID from Workday API Client registration'),
37
+ client_secret: z.string().describe('OAuth Client Secret'),
38
+ refresh_token: z.string().optional().describe('Optional refresh token. If omitted, client_credentials grant is used.'),
39
+ }),
40
+ annotations: { destructiveHint: false },
41
+ }, withErrorHandling(async (args) => {
42
+ const rawHost = args.host.trim();
43
+ const tenant = args.tenant.trim();
44
+ const cid = args.client_id.trim();
45
+ const csecret = args.client_secret.trim();
46
+ const rtoken = args.refresh_token?.trim() || undefined;
47
+ if (!rawHost || !tenant || !cid || !csecret) {
48
+ return JSON.stringify({ ok: false, error: 'host, tenant, client_id, and client_secret are all required.' });
49
+ }
50
+ // Normalize and validate host (SSRF prevention)
51
+ const hostValidation = validateHost(rawHost);
52
+ if (!hostValidation.valid) {
53
+ return JSON.stringify({ ok: false, error: hostValidation.error });
54
+ }
55
+ const host = hostValidation.host;
56
+ // Validate credentials by attempting token exchange + API probe
57
+ const authHeader = 'Basic ' + Buffer.from(`${cid}:${csecret}`).toString('base64');
58
+ const bodyParams = rtoken
59
+ ? { grant_type: 'refresh_token', refresh_token: rtoken }
60
+ : { grant_type: 'client_credentials' };
61
+ const body = new URLSearchParams(bodyParams);
62
+ const tokenUrl = `https://${host}/ccx/oauth2/${tenant}/token`;
63
+ let tokenResponse;
64
+ try {
65
+ tokenResponse = await fetch(tokenUrl, {
66
+ method: 'POST',
67
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
68
+ headers: {
69
+ Authorization: authHeader,
70
+ 'Content-Type': 'application/x-www-form-urlencoded',
71
+ Accept: 'application/json',
72
+ 'User-Agent': USER_AGENT,
73
+ },
74
+ body: body.toString(),
75
+ });
76
+ }
77
+ catch (error) {
78
+ if (error instanceof Error && error.name === 'TimeoutError') {
79
+ return JSON.stringify({
80
+ ok: false,
81
+ error: 'Token exchange request timed out.',
82
+ resolution: 'Verify the host domain is correct and accessible from your network.',
83
+ });
84
+ }
85
+ return JSON.stringify({
86
+ ok: false,
87
+ error: `Could not reach Workday: ${error instanceof Error ? error.message : String(error)}`,
88
+ resolution: 'Verify the host domain is correct and accessible from your network.',
89
+ });
90
+ }
91
+ if (!tokenResponse.ok) {
92
+ const errorBody = await tokenResponse.json().catch(() => ({}));
93
+ const detail = errorBody?.error_description || errorBody?.error || `HTTP ${tokenResponse.status}`;
94
+ return JSON.stringify({
95
+ ok: false,
96
+ error: `Token exchange failed: ${detail}`,
97
+ resolution: tokenResponse.status === 401
98
+ ? 'Check your Client ID and Client Secret. Ensure the API Client is registered correctly in Workday.'
99
+ : tokenResponse.status === 400
100
+ ? 'Check your tenant name and host domain. If using a refresh token, it may be expired or invalid.'
101
+ : `Unexpected error (${tokenResponse.status}). Verify host, tenant, and credentials.`,
102
+ });
103
+ }
104
+ const tokenData = await tokenResponse.json();
105
+ // API probe
106
+ const testUrl = `https://${host}/ccx/api/v1/${tenant}/workers?limit=1`;
107
+ let testResponse;
108
+ try {
109
+ testResponse = await fetch(testUrl, {
110
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
111
+ headers: {
112
+ Authorization: `Bearer ${tokenData.access_token}`,
113
+ Accept: 'application/json',
114
+ 'User-Agent': USER_AGENT,
115
+ },
116
+ });
117
+ }
118
+ catch (error) {
119
+ if (error instanceof Error && error.name === 'TimeoutError') {
120
+ return JSON.stringify({
121
+ ok: false,
122
+ error: 'API probe timed out after successful token exchange.',
123
+ resolution: 'Check network connectivity to Workday.',
124
+ });
125
+ }
126
+ return JSON.stringify({
127
+ ok: false,
128
+ error: `API probe failed: ${error instanceof Error ? error.message : String(error)}`,
129
+ resolution: 'Verify the host domain and network connectivity.',
130
+ });
131
+ }
132
+ if (!testResponse.ok) {
133
+ const status = testResponse.status;
134
+ return JSON.stringify({
135
+ ok: false,
136
+ error: `Token exchange succeeded but API probe failed (${status}).`,
137
+ resolution: status === 403
138
+ ? 'The ISU lacks permissions for the Workers REST API. Add the Integration System Security Group to the "Worker Data" domain in Workday.'
139
+ : status === 404
140
+ ? 'Workers endpoint not found. Verify the tenant name and that REST API is enabled.'
141
+ : `Unexpected API error (${status}). Check ISU permissions and REST API configuration.`,
142
+ });
143
+ }
144
+ // Persist via bridge
145
+ const result = await bridgeRequest('/bundled/workday/configure', {
146
+ host, tenant, clientId: cid, clientSecret: csecret, refreshToken: rtoken,
147
+ });
148
+ if (!result.success) {
149
+ throw new WorkdayError(result.error || 'Failed to configure Workday via bridge.', 'BRIDGE_ERROR', 'Check that the host application is running and bridge is available.');
150
+ }
151
+ // Update runtime credentials
152
+ setHost(host);
153
+ setTenant(tenant);
154
+ setClientId(cid);
155
+ setClientSecret(csecret);
156
+ setRefreshToken(rtoken ?? '');
157
+ clearTokenCache();
158
+ const message = result.warning
159
+ ? `Workday configured successfully. Note: ${result.warning}`
160
+ : 'Workday configured successfully! Try list_workday_workers to browse your team.';
161
+ return JSON.stringify({ ok: true, message });
162
+ }));
163
+ }
164
+ //# sourceMappingURL=configure.js.map
@@ -0,0 +1,4 @@
1
+ export { registerConfigureTools } from './configure.js';
2
+ export { registerWorkerTools } from './workers.js';
3
+ export { registerOrganizationTools } from './organizations.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ export { registerConfigureTools } from './configure.js';
2
+ export { registerWorkerTools } from './workers.js';
3
+ export { registerOrganizationTools } from './organizations.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Workday organization tools — list organizations.
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ export declare function registerOrganizationTools(server: McpServer): void;
6
+ //# sourceMappingURL=organizations.d.ts.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Workday organization tools — list organizations.
3
+ */
4
+ import { z } from 'zod';
5
+ import { ORG_LIST_FIELDS, pickFields, paginationHint } from '../types.js';
6
+ import { withErrorHandling } from '../utils.js';
7
+ import { isConfigured } from '../auth.js';
8
+ import { workdayFetch } from '../client.js';
9
+ export function registerOrganizationTools(server) {
10
+ server.registerTool('list_workday_organizations', {
11
+ description: `List organizations (departments, supervisory orgs, cost centers, etc.) in Workday.
12
+
13
+ Returns: ID, name/descriptor, type, active status.
14
+
15
+ Example: {}
16
+ Example: { "limit": 25, "offset": 50 }
17
+
18
+ Pagination: Returns up to 'limit' results (default 50, max 100). Use 'offset' for next page.
19
+
20
+ RELATED TOOLS:
21
+ - list_workday_workers: Browse workers in the organization
22
+ - get_workday_worker: See which organization a worker belongs to`,
23
+ inputSchema: z.object({
24
+ limit: z.number().optional().describe('Max results per page (default 50, max 100)'),
25
+ offset: z.number().optional().describe('Number of results to skip (for pagination, default 0)'),
26
+ }),
27
+ annotations: { readOnlyHint: true },
28
+ }, withErrorHandling(async (args) => {
29
+ if (!isConfigured()) {
30
+ return JSON.stringify({
31
+ ok: false,
32
+ error: 'Workday not configured',
33
+ resolution: 'Configure Workday with your OAuth credentials first.',
34
+ next_step: {
35
+ action: 'Ask the user for their Workday credentials, then call configure_workday_credentials',
36
+ tool_to_call: 'configure_workday_credentials',
37
+ },
38
+ });
39
+ }
40
+ const limit = Math.min(Math.max(Number(args.limit) || 50, 1), 100);
41
+ const offset = Math.max(Number(args.offset) || 0, 0);
42
+ const params = new URLSearchParams();
43
+ params.set('limit', String(limit));
44
+ params.set('offset', String(offset));
45
+ const result = await workdayFetch(`/organizations?${params.toString()}`);
46
+ const organizations = (result.data || []).map((o) => pickFields(o, ORG_LIST_FIELDS));
47
+ const total = result.total || organizations.length;
48
+ const hint = paginationHint(total, offset, organizations.length);
49
+ return JSON.stringify({ ok: true, organizations, count: organizations.length, total, pagination: hint });
50
+ }));
51
+ }
52
+ //# sourceMappingURL=organizations.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Workday worker tools — list and get worker profiles.
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ export declare function registerWorkerTools(server: McpServer): void;
6
+ //# sourceMappingURL=workers.d.ts.map
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Workday worker tools — list and get worker profiles.
3
+ */
4
+ import { z } from 'zod';
5
+ import { WORKER_LIST_FIELDS, WORKER_DETAIL_FIELDS, NESTED_OBJECT_FIELDS, pickFields, paginationHint, } from '../types.js';
6
+ import { withErrorHandling } from '../utils.js';
7
+ import { isConfigured } from '../auth.js';
8
+ import { workdayFetch } from '../client.js';
9
+ function notConfiguredResponse() {
10
+ return JSON.stringify({
11
+ ok: false,
12
+ error: 'Workday not configured',
13
+ resolution: 'Configure Workday with your OAuth credentials first.',
14
+ next_step: {
15
+ action: 'Ask the user for their Workday credentials, then call configure_workday_credentials',
16
+ tool_to_call: 'configure_workday_credentials',
17
+ },
18
+ });
19
+ }
20
+ export function registerWorkerTools(server) {
21
+ server.registerTool('list_workday_workers', {
22
+ description: `List or search workers (employees and contingent workers) in Workday.
23
+
24
+ Returns compact worker summaries: ID, name, email, title, manager status.
25
+
26
+ Example: {}
27
+ Example: { "search": "Jane Smith" }
28
+ Example: { "limit": 20, "offset": 100 }
29
+
30
+ Pagination: Returns up to 'limit' results (default 50, max 100). Use 'offset' for next page.
31
+
32
+ RELATED TOOLS:
33
+ - get_workday_worker: Pass a worker's id to get their full profile
34
+ - list_workday_organizations: Browse organizational structure
35
+
36
+ COMMON MISTAKES:
37
+ - search is a free-text filter (name, email, etc.) — not a Workday query language
38
+ - Maximum limit is 100 per request; use offset for pagination`,
39
+ inputSchema: z.object({
40
+ search: z.string().optional().describe('Free-text search filter (name, email, etc.)'),
41
+ limit: z.number().optional().describe('Max results per page (default 50, max 100)'),
42
+ offset: z.number().optional().describe('Number of results to skip (for pagination, default 0)'),
43
+ }),
44
+ annotations: { readOnlyHint: true },
45
+ }, withErrorHandling(async (args) => {
46
+ if (!isConfigured())
47
+ return notConfiguredResponse();
48
+ const limit = Math.min(Math.max(Number(args.limit) || 50, 1), 100);
49
+ const offset = Math.max(Number(args.offset) || 0, 0);
50
+ const params = new URLSearchParams();
51
+ params.set('limit', String(limit));
52
+ params.set('offset', String(offset));
53
+ if (args.search)
54
+ params.set('search', args.search);
55
+ const result = await workdayFetch(`/workers?${params.toString()}`);
56
+ const workers = (result.data || []).map((w) => pickFields(w, WORKER_LIST_FIELDS));
57
+ const total = result.total || workers.length;
58
+ const hint = paginationHint(total, offset, workers.length);
59
+ return JSON.stringify({ ok: true, workers, count: workers.length, total, pagination: hint });
60
+ }));
61
+ server.registerTool('get_workday_worker', {
62
+ description: `Get a worker's full profile by ID from Workday.
63
+
64
+ Returns detailed profile: name, email, title, manager status, location,
65
+ supervisory organization, years of service.
66
+
67
+ Example: { "worker_id": "3aa5550b7fe348b98d7b5741afc65534" }
68
+
69
+ WORKFLOW - To find a worker:
70
+ 1. Call list_workday_workers to search by name or email
71
+ 2. Use the worker's id from the results here
72
+
73
+ RELATED TOOLS:
74
+ - list_workday_workers: Search/browse workers to find IDs
75
+ - list_workday_organizations: See org structure`,
76
+ inputSchema: z.object({
77
+ worker_id: z.string().describe('Worker ID (from list_workday_workers)'),
78
+ }),
79
+ annotations: { readOnlyHint: true },
80
+ }, withErrorHandling(async (args) => {
81
+ if (!isConfigured())
82
+ return notConfiguredResponse();
83
+ const worker = await workdayFetch(`/workers/${encodeURIComponent(args.worker_id)}`);
84
+ const filtered = pickFields(worker, WORKER_DETAIL_FIELDS);
85
+ // Deep-pick nested objects to prevent PII leakage from sub-fields
86
+ if (worker.location && typeof worker.location === 'object') {
87
+ filtered.location = pickFields(worker.location, NESTED_OBJECT_FIELDS);
88
+ }
89
+ if (worker.supervisoryOrganization && typeof worker.supervisoryOrganization === 'object') {
90
+ filtered.supervisoryOrganization = pickFields(worker.supervisoryOrganization, NESTED_OBJECT_FIELDS);
91
+ }
92
+ return JSON.stringify({ ok: true, worker: filtered });
93
+ }));
94
+ }
95
+ //# sourceMappingURL=workers.js.map
@@ -0,0 +1,18 @@
1
+ export declare const REQUEST_TIMEOUT_MS = 30000;
2
+ export declare const USER_AGENT = "MindstoneRebel/1.0 (Workday-MCP)";
3
+ export interface BridgeState {
4
+ port: number;
5
+ token: string;
6
+ }
7
+ export declare class WorkdayError extends Error {
8
+ readonly code: string;
9
+ readonly resolution: string;
10
+ constructor(message: string, code: string, resolution: string);
11
+ }
12
+ export declare const WORKER_LIST_FIELDS: readonly ["id", "descriptor", "primaryWorkEmail", "businessTitle", "isManager"];
13
+ export declare const WORKER_DETAIL_FIELDS: readonly ["id", "descriptor", "primaryWorkEmail", "businessTitle", "isManager", "yearsOfService", "href"];
14
+ export declare const NESTED_OBJECT_FIELDS: readonly ["id", "descriptor"];
15
+ export declare const ORG_LIST_FIELDS: readonly ["id", "descriptor", "type", "isActive", "href"];
16
+ export declare function pickFields<T extends readonly string[]>(obj: Record<string, unknown>, fields: T): Record<string, unknown>;
17
+ export declare function paginationHint(total: number, offset: number, count: number): string;
18
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,35 @@
1
+ export const REQUEST_TIMEOUT_MS = 30_000;
2
+ export const USER_AGENT = 'MindstoneRebel/1.0 (Workday-MCP)';
3
+ export class WorkdayError extends Error {
4
+ code;
5
+ resolution;
6
+ constructor(message, code, resolution) {
7
+ super(message);
8
+ this.code = code;
9
+ this.resolution = resolution;
10
+ this.name = 'WorkdayError';
11
+ }
12
+ }
13
+ // ── Allowlisted response fields ──
14
+ export const WORKER_LIST_FIELDS = ['id', 'descriptor', 'primaryWorkEmail', 'businessTitle', 'isManager'];
15
+ export const WORKER_DETAIL_FIELDS = ['id', 'descriptor', 'primaryWorkEmail', 'businessTitle', 'isManager', 'yearsOfService', 'href'];
16
+ export const NESTED_OBJECT_FIELDS = ['id', 'descriptor'];
17
+ export const ORG_LIST_FIELDS = ['id', 'descriptor', 'type', 'isActive', 'href'];
18
+ // ── Field allowlisting ──
19
+ export function pickFields(obj, fields) {
20
+ const result = {};
21
+ for (const field of fields) {
22
+ if (field in obj) {
23
+ result[field] = obj[field];
24
+ }
25
+ }
26
+ return result;
27
+ }
28
+ // ── Pagination helper ──
29
+ export function paginationHint(total, offset, count) {
30
+ if (count >= total)
31
+ return `Showing all ${total} results.`;
32
+ const remaining = total - offset - count;
33
+ return `Showing ${count} of ${total} total (offset=${offset}). ${remaining > 0 ? `Use offset=${offset + count} to see more.` : ''}`;
34
+ }
35
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,14 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ type ToolHandler<T> = (args: T, extra: unknown) => Promise<CallToolResult>;
3
+ /**
4
+ * Wraps a tool handler with standard error handling.
5
+ *
6
+ * - On success: returns the string result as a text content block.
7
+ * - On WorkdayError: returns a structured JSON error with code and resolution.
8
+ * - On unknown error: returns a generic error message.
9
+ *
10
+ * Secrets are never exposed in error messages.
11
+ */
12
+ export declare function withErrorHandling<T>(fn: (args: T, extra: unknown) => Promise<string>): ToolHandler<T>;
13
+ export {};
14
+ //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js ADDED
@@ -0,0 +1,42 @@
1
+ import { WorkdayError } from './types.js';
2
+ /**
3
+ * Wraps a tool handler with standard error handling.
4
+ *
5
+ * - On success: returns the string result as a text content block.
6
+ * - On WorkdayError: returns a structured JSON error with code and resolution.
7
+ * - On unknown error: returns a generic error message.
8
+ *
9
+ * Secrets are never exposed in error messages.
10
+ */
11
+ export function withErrorHandling(fn) {
12
+ return async (args, extra) => {
13
+ try {
14
+ const result = await fn(args, extra);
15
+ return { content: [{ type: 'text', text: result }] };
16
+ }
17
+ catch (error) {
18
+ if (error instanceof WorkdayError) {
19
+ return {
20
+ content: [
21
+ {
22
+ type: 'text',
23
+ text: JSON.stringify({
24
+ ok: false,
25
+ error: error.message,
26
+ code: error.code,
27
+ resolution: error.resolution,
28
+ }),
29
+ },
30
+ ],
31
+ isError: true,
32
+ };
33
+ }
34
+ const errorMessage = error instanceof Error ? error.message : String(error);
35
+ return {
36
+ content: [{ type: 'text', text: JSON.stringify({ ok: false, error: errorMessage }) }],
37
+ isError: true,
38
+ };
39
+ }
40
+ };
41
+ }
42
+ //# sourceMappingURL=utils.js.map
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@mindstone-engineering/mcp-server-workday",
3
+ "version": "0.1.0",
4
+ "description": "Workday HCM MCP server for Model Context Protocol hosts — workers, profiles, organizations",
5
+ "license": "FSL-1.1-MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-server-workday": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "!dist/**/*.map"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/nspr-io/mcp-servers.git",
17
+ "directory": "connectors/workday"
18
+ },
19
+ "homepage": "https://github.com/nspr-io/mcp-servers/tree/main/connectors/workday",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc && shx chmod +x dist/index.js",
25
+ "prepare": "npm run build",
26
+ "watch": "tsc --watch",
27
+ "start": "node dist/index.js",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "test:coverage": "vitest run --coverage"
31
+ },
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.26.0",
34
+ "zod": "^3.23.0"
35
+ },
36
+ "devDependencies": {
37
+ "@mindstone-engineering/mcp-test-harness": "file:../../test-harness",
38
+ "@types/node": "^22",
39
+ "@vitest/coverage-v8": "^4.1.3",
40
+ "msw": "^2.13.2",
41
+ "shx": "^0.3.4",
42
+ "typescript": "^5.8.2",
43
+ "vitest": "^4.1.3"
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ }
48
+ }