@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 +34 -0
- package/dist/auth.js +181 -0
- package/dist/bridge.d.ts +16 -0
- package/dist/bridge.js +43 -0
- package/dist/client.d.ts +14 -0
- package/dist/client.js +81 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +33 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +13 -0
- package/dist/tools/configure.d.ts +9 -0
- package/dist/tools/configure.js +164 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/organizations.d.ts +6 -0
- package/dist/tools/organizations.js +52 -0
- package/dist/tools/workers.d.ts +6 -0
- package/dist/tools/workers.js +95 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.js +35 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.js +42 -0
- package/package.json +48 -0
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
|
package/dist/bridge.d.ts
ADDED
|
@@ -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
|
package/dist/client.d.ts
ADDED
|
@@ -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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
package/dist/server.d.ts
ADDED
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,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,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
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
package/dist/utils.d.ts
ADDED
|
@@ -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
|
+
}
|