trackly-cli 0.7.3 → 0.9.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/README.md +18 -6
- package/bin/trackly +2 -0
- package/contracts/trackly-apply-tools.json +26 -7
- package/docs/trackly-tools.md +14 -2
- package/lib/agent.js +197 -11
- package/lib/client.js +40 -2
- package/mcp/apply-tools.js +666 -0
- package/mcp/server.js +5 -204
- package/npm-shrinkwrap.json +1177 -0
- package/package.json +12 -4
- package/scripts/verify-audit-exceptions.js +165 -0
- package/security/audit-exceptions.json +4 -0
- package/server.json +2 -2
- package/skills/trackly-apply/SKILL.md +26 -17
- package/skills/trackly-apply/agents/openai.yaml +1 -1
- package/skills/trackly-apply/references/ats-playbook.md +6 -0
- package/skills/trackly-apply/references/batch-orchestration.md +208 -0
- package/skills/trackly-apply/references/browser-lifecycle.md +101 -0
- package/skills/trackly-apply/references/review-handoff.md +53 -2
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { z } = require('zod');
|
|
4
|
+
const { apiRequest } = require('../lib/client');
|
|
5
|
+
const { prepareResume, verifyPreparedResume } = require('../lib/agent');
|
|
6
|
+
const APPLY_CONTRACT = require('../contracts/trackly-apply-tools.json');
|
|
7
|
+
|
|
8
|
+
const APPLY_BROWSER_SURFACES = APPLY_CONTRACT.constants.applyBrowserSurfaces;
|
|
9
|
+
const APPLY_SCENARIO_CODES = APPLY_CONTRACT.constants.applyScenarioCodes;
|
|
10
|
+
const APPLY_CHECKPOINT_ACTION_CODES = APPLY_CONTRACT.constants.applyCheckpointActionCodes;
|
|
11
|
+
const APPLY_CHECKPOINT_PACKET_PHASES = APPLY_CONTRACT.constants.applyCheckpointPacketPhases;
|
|
12
|
+
const APPLY_SURFACE_BINDING_REASONS = APPLY_CONTRACT.constants.applySurfaceBindingReasons;
|
|
13
|
+
const APPLY_SURFACE_EVIDENCE_TYPES = APPLY_CONTRACT.constants.applySurfaceEvidenceTypes;
|
|
14
|
+
const APPLY_SURFACE_OWNERSHIP_STATES = APPLY_CONTRACT.constants.applySurfaceOwnershipStates;
|
|
15
|
+
const APPLY_SUBMISSION_EVIDENCE_TYPES = APPLY_CONTRACT.constants.applySubmissionEvidenceTypes;
|
|
16
|
+
const APPLY_SUBMISSION_EVIDENCE_SOURCES = APPLY_CONTRACT.constants.applySubmissionEvidenceSources;
|
|
17
|
+
const APPLY_BATCH_MAX_MEMBERS = 100;
|
|
18
|
+
const APPLY_BATCH_MAX_CHECKPOINTS_PER_REQUEST = 20;
|
|
19
|
+
const APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT = 25;
|
|
20
|
+
const APPLY_BATCH_MAX_BULK_MUTATIONS = 20;
|
|
21
|
+
const SAFE_OBSERVATION_CODE = /^[a-z0-9][a-z0-9_:-]{0,99}$/;
|
|
22
|
+
const SAFE_IDEMPOTENCY_KEY = /^[\x20-\x7e]+$/;
|
|
23
|
+
|
|
24
|
+
const truthCertificationCommon = {
|
|
25
|
+
batchId: z.number().int().min(1),
|
|
26
|
+
leaseToken: z.string().min(1).max(1024),
|
|
27
|
+
membershipHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
28
|
+
profileRevision: z.number().int().min(0),
|
|
29
|
+
memberRuns: z.array(z.object({
|
|
30
|
+
memberId: z.number().int().min(1),
|
|
31
|
+
runId: z.number().int().min(1),
|
|
32
|
+
memberVersion: z.number().int().min(1),
|
|
33
|
+
inspectionEpoch: z.number().int().min(0),
|
|
34
|
+
})).min(1).max(100),
|
|
35
|
+
answerSnapshotHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
36
|
+
wordingFingerprint: z.string().regex(/^[a-f0-9]{64}$/),
|
|
37
|
+
expiresAt: z.string().datetime(),
|
|
38
|
+
idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// The MCP SDK can publish only top-level object schemas through tools/list.
|
|
42
|
+
// Keep discovery concrete, then apply the exact cross-field invariant in the handler.
|
|
43
|
+
const truthCertificationInputSchema = z.object({
|
|
44
|
+
...truthCertificationCommon,
|
|
45
|
+
resumeDependency: z.enum(['approved', 'not_applicable']),
|
|
46
|
+
resumeId: z.number().int().min(1).nullable().optional(),
|
|
47
|
+
resumeSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable().optional(),
|
|
48
|
+
});
|
|
49
|
+
const truthCertificationSchema = z.discriminatedUnion('resumeDependency', [
|
|
50
|
+
z.object({
|
|
51
|
+
...truthCertificationCommon,
|
|
52
|
+
resumeDependency: z.literal('approved'),
|
|
53
|
+
resumeId: z.number().int().min(1),
|
|
54
|
+
resumeSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
55
|
+
}),
|
|
56
|
+
z.object({
|
|
57
|
+
...truthCertificationCommon,
|
|
58
|
+
resumeDependency: z.literal('not_applicable'),
|
|
59
|
+
resumeId: z.null().optional(),
|
|
60
|
+
resumeSha256: z.null().optional(),
|
|
61
|
+
}),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const startApplyRunInputSchema = z.object({
|
|
65
|
+
jobId: z.number().int().min(1),
|
|
66
|
+
clientName: z.string().max(100).optional(),
|
|
67
|
+
batchId: z.number().int().min(1).optional(),
|
|
68
|
+
memberId: z.number().int().min(1).optional(),
|
|
69
|
+
expectedMemberVersion: z.number().int().min(1).optional(),
|
|
70
|
+
expectedInspectionEpoch: z.number().int().min(0).optional(),
|
|
71
|
+
leaseToken: z.string().min(1).max(1024).optional(),
|
|
72
|
+
});
|
|
73
|
+
const startApplyRunSchema = z.object({
|
|
74
|
+
jobId: z.number().int().min(1),
|
|
75
|
+
clientName: z.string().max(100).optional(),
|
|
76
|
+
batchId: z.number().int().min(1).optional(),
|
|
77
|
+
memberId: z.number().int().min(1).optional(),
|
|
78
|
+
expectedMemberVersion: z.number().int().min(1).optional(),
|
|
79
|
+
expectedInspectionEpoch: z.number().int().min(0).optional(),
|
|
80
|
+
leaseToken: z.string().min(1).max(1024).optional(),
|
|
81
|
+
}).superRefine((value, context) => {
|
|
82
|
+
const batchValues = [
|
|
83
|
+
value.batchId,
|
|
84
|
+
value.memberId,
|
|
85
|
+
value.expectedMemberVersion,
|
|
86
|
+
value.expectedInspectionEpoch,
|
|
87
|
+
value.leaseToken,
|
|
88
|
+
];
|
|
89
|
+
if (
|
|
90
|
+
batchValues.some((item) => item !== undefined)
|
|
91
|
+
&& batchValues.some((item) => item === undefined)
|
|
92
|
+
) {
|
|
93
|
+
context.addIssue({
|
|
94
|
+
code: z.ZodIssueCode.custom,
|
|
95
|
+
message: 'Batch binding fields must be supplied together',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
function registerApplyTools(
|
|
101
|
+
server,
|
|
102
|
+
{
|
|
103
|
+
wrapTool,
|
|
104
|
+
mcpUserAgent: MCP_USER_AGENT,
|
|
105
|
+
throwMcpResourceError,
|
|
106
|
+
},
|
|
107
|
+
) {
|
|
108
|
+
server.tool(
|
|
109
|
+
'trackly_get_apply_queue',
|
|
110
|
+
'Get the deterministic queue of jobs the user already approved by saving as check later. Do not rescore or veto these jobs.',
|
|
111
|
+
{
|
|
112
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
113
|
+
cursor: z.string().min(1).max(2048).optional(),
|
|
114
|
+
},
|
|
115
|
+
wrapTool(async ({ limit, cursor }) => {
|
|
116
|
+
const qs = new URLSearchParams();
|
|
117
|
+
if (limit !== undefined) qs.set('limit', String(limit));
|
|
118
|
+
if (cursor) qs.set('cursor', cursor);
|
|
119
|
+
const query = qs.toString();
|
|
120
|
+
return apiRequest('GET', `/api/jobscout/apply/queue${query ? `?${query}` : ''}`, null, false, false, MCP_USER_AGENT);
|
|
121
|
+
}, 'Failed to fetch apply queue')
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
server.tool(
|
|
125
|
+
'trackly_get_application_profile',
|
|
126
|
+
'Get the versioned application profile. Sensitive values are returned only after the user opted into encrypted storage.',
|
|
127
|
+
{
|
|
128
|
+
includeSensitive: z.boolean().optional(),
|
|
129
|
+
provider: z.string().max(100).optional(),
|
|
130
|
+
companyId: z.string().max(100).optional(),
|
|
131
|
+
},
|
|
132
|
+
wrapTool(async ({ includeSensitive, provider, companyId }) => {
|
|
133
|
+
const qs = new URLSearchParams();
|
|
134
|
+
if (includeSensitive) qs.set('includeSensitive', 'true');
|
|
135
|
+
if (provider) qs.set('provider', provider);
|
|
136
|
+
if (companyId) qs.set('companyId', companyId);
|
|
137
|
+
return apiRequest('GET', `/api/jobscout/application-profile?${qs.toString()}`, null, false, false, MCP_USER_AGENT);
|
|
138
|
+
}, 'Failed to fetch application profile')
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
server.tool(
|
|
142
|
+
'trackly_get_profile_onboarding',
|
|
143
|
+
'Get the backend-owned profile schema and onboarding questions. Ask only fields whose state is unknown or needs confirmation.',
|
|
144
|
+
{},
|
|
145
|
+
wrapTool(async () => {
|
|
146
|
+
const [schema, profile] = await Promise.all([
|
|
147
|
+
apiRequest('GET', '/api/jobscout/application-profile/schema', null, false, false, MCP_USER_AGENT),
|
|
148
|
+
apiRequest('GET', '/api/jobscout/application-profile', null, false, false, MCP_USER_AGENT),
|
|
149
|
+
]);
|
|
150
|
+
return { schema, profile };
|
|
151
|
+
}, 'Failed to fetch profile onboarding')
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
server.tool(
|
|
155
|
+
'trackly_update_application_profile',
|
|
156
|
+
'Update confirmed profile answers with optimistic concurrency. Use global scope only for an explicit always-answer preference.',
|
|
157
|
+
{
|
|
158
|
+
expectedRevision: z.number().int().min(1),
|
|
159
|
+
source: z.enum(['web', 'ios', 'macos', 'codex', 'claude', 'mcp']).optional(),
|
|
160
|
+
changes: z.array(z.discriminatedUnion('scope', [
|
|
161
|
+
z.object({
|
|
162
|
+
key: z.string().min(1).max(200), state: z.enum(['unknown', 'answered', 'intentionally_blank', 'declined']),
|
|
163
|
+
value: z.any().optional(), scope: z.literal('global'), questionLabel: z.string().max(1000).optional(),
|
|
164
|
+
}),
|
|
165
|
+
z.object({
|
|
166
|
+
key: z.string().min(1).max(200), state: z.enum(['unknown', 'answered', 'intentionally_blank', 'declined']),
|
|
167
|
+
value: z.any().optional(), scope: z.literal('provider'), scopeValue: z.string().min(1).max(200),
|
|
168
|
+
questionLabel: z.string().max(1000).optional(),
|
|
169
|
+
}),
|
|
170
|
+
z.object({
|
|
171
|
+
key: z.string().min(1).max(200), state: z.enum(['unknown', 'answered', 'intentionally_blank', 'declined']),
|
|
172
|
+
value: z.any().optional(), scope: z.literal('company'), scopeValue: z.string().min(1).max(200),
|
|
173
|
+
questionLabel: z.string().max(1000).optional(),
|
|
174
|
+
}),
|
|
175
|
+
])).max(100).optional(),
|
|
176
|
+
education: z.array(z.object({
|
|
177
|
+
school: z.string().min(1).max(500),
|
|
178
|
+
degree: z.string().max(500).nullable().optional(),
|
|
179
|
+
fieldOfStudy: z.string().max(500).nullable().optional(),
|
|
180
|
+
gpa: z.string().max(50).nullable().optional(),
|
|
181
|
+
startDate: z.string().max(50).nullable().optional(),
|
|
182
|
+
endDate: z.string().max(50).nullable().optional(),
|
|
183
|
+
})).max(20).optional(),
|
|
184
|
+
confirmProfile: z.boolean().optional(),
|
|
185
|
+
sensitiveStorageConsent: z.boolean().optional(),
|
|
186
|
+
},
|
|
187
|
+
wrapTool(async (params) => apiRequest('PATCH', '/api/jobscout/application-profile', params, false, false, MCP_USER_AGENT), 'Failed to update application profile')
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
server.tool(
|
|
191
|
+
'trackly_create_apply_batch',
|
|
192
|
+
'Freeze an exact recent-first set of approved Check Later jobs before browser work. New queue entries never change this batch.',
|
|
193
|
+
{
|
|
194
|
+
limit: z.number().int().min(1).max(100),
|
|
195
|
+
idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
|
|
196
|
+
},
|
|
197
|
+
wrapTool(async ({ limit, idempotencyKey }) => apiRequest(
|
|
198
|
+
'POST',
|
|
199
|
+
'/api/jobscout/apply/batches',
|
|
200
|
+
{ limit },
|
|
201
|
+
false,
|
|
202
|
+
false,
|
|
203
|
+
MCP_USER_AGENT,
|
|
204
|
+
{ 'Idempotency-Key': idempotencyKey }
|
|
205
|
+
), 'Failed to create apply batch')
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
server.tool(
|
|
209
|
+
'trackly_get_apply_batch',
|
|
210
|
+
'Read an existing frozen Apply batch by opaque server pagination. Do not reorder, replace, or rescore members.',
|
|
211
|
+
{
|
|
212
|
+
batchId: z.number().int().min(1),
|
|
213
|
+
limit: z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),
|
|
214
|
+
cursor: z.string().min(1).max(2048).optional(),
|
|
215
|
+
actionLimit: z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),
|
|
216
|
+
actionCursor: z.string().min(1).max(2048).optional(),
|
|
217
|
+
},
|
|
218
|
+
wrapTool(async ({ batchId, limit, cursor, actionLimit, actionCursor }) => {
|
|
219
|
+
const qs = new URLSearchParams();
|
|
220
|
+
if (limit !== undefined) qs.set('limit', String(limit));
|
|
221
|
+
if (cursor) qs.set('cursor', cursor);
|
|
222
|
+
if (actionLimit !== undefined) qs.set('actionLimit', String(actionLimit));
|
|
223
|
+
if (actionCursor) qs.set('actionCursor', actionCursor);
|
|
224
|
+
const query = qs.toString();
|
|
225
|
+
return apiRequest(
|
|
226
|
+
'GET',
|
|
227
|
+
`/api/jobscout/apply/batches/${batchId}${query ? `?${query}` : ''}`,
|
|
228
|
+
null,
|
|
229
|
+
false,
|
|
230
|
+
false,
|
|
231
|
+
MCP_USER_AGENT
|
|
232
|
+
);
|
|
233
|
+
}, 'Failed to fetch apply batch')
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
server.tool(
|
|
237
|
+
'trackly_get_active_apply_batch',
|
|
238
|
+
'Recover the newest unexpired frozen Apply batch for this user after context loss. Returns active=false when no resumable batch exists.',
|
|
239
|
+
{
|
|
240
|
+
limit: z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),
|
|
241
|
+
cursor: z.string().min(1).max(2048).optional(),
|
|
242
|
+
actionLimit: z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),
|
|
243
|
+
actionCursor: z.string().min(1).max(2048).optional(),
|
|
244
|
+
},
|
|
245
|
+
wrapTool(async ({ limit, cursor, actionLimit, actionCursor }) => {
|
|
246
|
+
const qs = new URLSearchParams();
|
|
247
|
+
if (limit !== undefined) qs.set('limit', String(limit));
|
|
248
|
+
if (cursor) qs.set('cursor', cursor);
|
|
249
|
+
if (actionLimit !== undefined) qs.set('actionLimit', String(actionLimit));
|
|
250
|
+
if (actionCursor) qs.set('actionCursor', actionCursor);
|
|
251
|
+
const query = qs.toString();
|
|
252
|
+
return apiRequest(
|
|
253
|
+
'GET',
|
|
254
|
+
`/api/jobscout/apply/batches/active${query ? `?${query}` : ''}`,
|
|
255
|
+
null,
|
|
256
|
+
false,
|
|
257
|
+
false,
|
|
258
|
+
MCP_USER_AGENT
|
|
259
|
+
);
|
|
260
|
+
}, 'Failed to recover active apply batch')
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
server.tool(
|
|
264
|
+
'trackly_claim_apply_batch',
|
|
265
|
+
'Acquire or renew the optimistic lease required before mutating browser-bound batch members.',
|
|
266
|
+
{
|
|
267
|
+
batchId: z.number().int().min(1),
|
|
268
|
+
expectedRevision: z.number().int().min(1),
|
|
269
|
+
leaseOwner: z.string().min(1).max(1024),
|
|
270
|
+
leaseToken: z.string().min(1).max(1024),
|
|
271
|
+
leaseDurationMs: z.number().int().min(15000).max(300000),
|
|
272
|
+
},
|
|
273
|
+
wrapTool(async ({ batchId, ...body }) => apiRequest(
|
|
274
|
+
'POST',
|
|
275
|
+
`/api/jobscout/apply/batches/${batchId}/claim`,
|
|
276
|
+
body,
|
|
277
|
+
false,
|
|
278
|
+
false,
|
|
279
|
+
MCP_USER_AGENT
|
|
280
|
+
), 'Failed to claim apply batch')
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
server.tool(
|
|
284
|
+
'trackly_checkpoint_apply_batch',
|
|
285
|
+
'Bulk-checkpoint up to 20 browser inspections. Persist only typed actions and redacted fingerprints; never send labels, options, answers, credentials, OTPs, CAPTCHA text, or page content.',
|
|
286
|
+
{
|
|
287
|
+
batchId: z.number().int().min(1),
|
|
288
|
+
leaseToken: z.string().min(1).max(1024),
|
|
289
|
+
checkpoints: z.array(z.object({
|
|
290
|
+
memberId: z.number().int().min(1),
|
|
291
|
+
runId: z.number().int().min(1),
|
|
292
|
+
expectedMemberVersion: z.number().int().min(1),
|
|
293
|
+
expectedInspectionEpoch: z.number().int().min(0),
|
|
294
|
+
inspectionEpoch: z.number().int().min(0),
|
|
295
|
+
packetPhase: z.enum(APPLY_CHECKPOINT_PACKET_PHASES).optional(),
|
|
296
|
+
knownFieldsCommitted: z.boolean(),
|
|
297
|
+
resolvedActionIds: z.array(z.string().regex(/^[1-9][0-9]*$/))
|
|
298
|
+
.max(APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT).optional(),
|
|
299
|
+
idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
|
|
300
|
+
actions: z.array(z.object({
|
|
301
|
+
actionCode: z.enum(APPLY_CHECKPOINT_ACTION_CODES),
|
|
302
|
+
continuationAllowed: z.boolean(),
|
|
303
|
+
fieldFingerprint: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
304
|
+
})).min(1).max(APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT),
|
|
305
|
+
})).min(1).max(APPLY_BATCH_MAX_CHECKPOINTS_PER_REQUEST),
|
|
306
|
+
},
|
|
307
|
+
wrapTool(async ({ batchId, ...body }) => apiRequest(
|
|
308
|
+
'POST',
|
|
309
|
+
`/api/jobscout/apply/batches/${batchId}/checkpoints`,
|
|
310
|
+
body,
|
|
311
|
+
false,
|
|
312
|
+
false,
|
|
313
|
+
MCP_USER_AGENT
|
|
314
|
+
), 'Failed to checkpoint apply batch')
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
server.tool(
|
|
318
|
+
'trackly_approve_apply_batch_resume',
|
|
319
|
+
'Record one explicit approval for the exact default-resume identity and immutable current run set. Every local attachment still requires immediate path/hash verification.',
|
|
320
|
+
{
|
|
321
|
+
batchId: z.number().int().min(1),
|
|
322
|
+
leaseToken: z.string().min(1).max(1024),
|
|
323
|
+
membershipHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
324
|
+
profileRevision: z.number().int().min(0),
|
|
325
|
+
resumeId: z.number().int().min(1),
|
|
326
|
+
resumeSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
327
|
+
resumeFilename: z.string().min(1).max(255),
|
|
328
|
+
resumeSizeBytes: z.number().int().min(1),
|
|
329
|
+
memberRuns: z.array(z.object({
|
|
330
|
+
memberId: z.number().int().min(1),
|
|
331
|
+
runId: z.number().int().min(1),
|
|
332
|
+
memberVersion: z.number().int().min(1),
|
|
333
|
+
inspectionEpoch: z.number().int().min(0),
|
|
334
|
+
})).min(1).max(100),
|
|
335
|
+
expiresAt: z.string().datetime(),
|
|
336
|
+
idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
|
|
337
|
+
},
|
|
338
|
+
wrapTool(async ({ batchId, idempotencyKey, ...body }) => apiRequest(
|
|
339
|
+
'POST',
|
|
340
|
+
`/api/jobscout/apply/batches/${batchId}/resume-approval`,
|
|
341
|
+
body,
|
|
342
|
+
false,
|
|
343
|
+
false,
|
|
344
|
+
MCP_USER_AGENT,
|
|
345
|
+
{ 'Idempotency-Key': idempotencyKey }
|
|
346
|
+
), 'Failed to approve batch resume')
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
server.tool(
|
|
350
|
+
'trackly_bind_apply_surface',
|
|
351
|
+
'Bind an initial or recovered browser surface to an existing frozen member and run. This increments the inspection epoch and returns only the exact backend-stored requisition URL; it never creates a replacement run.',
|
|
352
|
+
{
|
|
353
|
+
batchId: z.number().int().min(1),
|
|
354
|
+
memberId: z.number().int().min(1),
|
|
355
|
+
runId: z.number().int().min(1),
|
|
356
|
+
expectedMemberVersion: z.number().int().min(1),
|
|
357
|
+
expectedInspectionEpoch: z.number().int().min(0),
|
|
358
|
+
leaseToken: z.string().min(1).max(1024),
|
|
359
|
+
browserBindingHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
360
|
+
browserSurface: z.enum(APPLY_BROWSER_SURFACES),
|
|
361
|
+
adapterCode: z.string().regex(SAFE_OBSERVATION_CODE),
|
|
362
|
+
bindingReason: z.enum(APPLY_SURFACE_BINDING_REASONS),
|
|
363
|
+
idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
|
|
364
|
+
},
|
|
365
|
+
wrapTool(async ({ batchId, memberId, idempotencyKey, ...body }) => apiRequest(
|
|
366
|
+
'POST',
|
|
367
|
+
`/api/jobscout/apply/batches/${batchId}/members/${memberId}/surface-binding`,
|
|
368
|
+
body,
|
|
369
|
+
false,
|
|
370
|
+
false,
|
|
371
|
+
MCP_USER_AGENT,
|
|
372
|
+
{ 'Idempotency-Key': idempotencyKey }
|
|
373
|
+
), 'Failed to bind apply browser surface')
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
server.tool(
|
|
377
|
+
'trackly_record_apply_surface_evidence',
|
|
378
|
+
'Record value-free current-epoch inventory, missing-tab, close-receipt, post-close absence, or close-failure evidence. closed_verified requires complete controller+user union inventory, an explicit close receipt, and post-close union absence.',
|
|
379
|
+
{
|
|
380
|
+
batchId: z.number().int().min(1),
|
|
381
|
+
memberId: z.number().int().min(1),
|
|
382
|
+
runId: z.number().int().min(1),
|
|
383
|
+
expectedMemberVersion: z.number().int().min(1),
|
|
384
|
+
expectedInspectionEpoch: z.number().int().min(1),
|
|
385
|
+
leaseToken: z.string().min(1).max(1024),
|
|
386
|
+
browserBindingHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
387
|
+
browserSurface: z.enum(APPLY_BROWSER_SURFACES),
|
|
388
|
+
adapterCode: z.string().regex(SAFE_OBSERVATION_CODE),
|
|
389
|
+
ownershipState: z.enum(APPLY_SURFACE_OWNERSHIP_STATES),
|
|
390
|
+
completeInventory: z.boolean(),
|
|
391
|
+
evidenceType: z.enum(APPLY_SURFACE_EVIDENCE_TYPES),
|
|
392
|
+
idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
|
|
393
|
+
},
|
|
394
|
+
wrapTool(async ({ batchId, memberId, idempotencyKey, ...body }) => apiRequest(
|
|
395
|
+
'POST',
|
|
396
|
+
`/api/jobscout/apply/batches/${batchId}/members/${memberId}/surface-evidence`,
|
|
397
|
+
body,
|
|
398
|
+
false,
|
|
399
|
+
false,
|
|
400
|
+
MCP_USER_AGENT,
|
|
401
|
+
{ 'Idempotency-Key': idempotencyKey }
|
|
402
|
+
), 'Failed to record apply browser surface evidence')
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
server.tool(
|
|
406
|
+
'trackly_record_apply_submission_evidence',
|
|
407
|
+
'Record redacted request, success-page, explicit user-confirmation, or provider-receipt evidence for the current batch member and inspection epoch. Never send page text, receipt identifiers, or external references.',
|
|
408
|
+
{
|
|
409
|
+
batchId: z.number().int().min(1),
|
|
410
|
+
memberId: z.number().int().min(1),
|
|
411
|
+
runId: z.number().int().min(1),
|
|
412
|
+
expectedMemberVersion: z.number().int().min(1),
|
|
413
|
+
expectedInspectionEpoch: z.number().int().min(1),
|
|
414
|
+
leaseToken: z.string().min(1).max(1024),
|
|
415
|
+
browserBindingHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
416
|
+
evidenceType: z.enum(APPLY_SUBMISSION_EVIDENCE_TYPES),
|
|
417
|
+
evidenceSource: z.enum(APPLY_SUBMISSION_EVIDENCE_SOURCES),
|
|
418
|
+
evidenceFingerprint: z.string().regex(/^[a-f0-9]{64}$/),
|
|
419
|
+
idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
|
|
420
|
+
},
|
|
421
|
+
wrapTool(async ({ batchId, memberId, idempotencyKey, ...body }) => apiRequest(
|
|
422
|
+
'POST',
|
|
423
|
+
`/api/jobscout/apply/batches/${batchId}/members/${memberId}/submission-evidence`,
|
|
424
|
+
body,
|
|
425
|
+
false,
|
|
426
|
+
false,
|
|
427
|
+
MCP_USER_AGENT,
|
|
428
|
+
{ 'Idempotency-Key': idempotencyKey }
|
|
429
|
+
), 'Failed to record apply submission evidence')
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
server.registerTool(
|
|
433
|
+
'trackly_certify_apply_batch_truth',
|
|
434
|
+
{
|
|
435
|
+
description: 'Record a late, expiring truthfulness certification over final answer and wording fingerprints for the exact complete subset that is currently review-ready. Unresolved members remain resumable and do not block ready siblings. This never becomes a profile answer.',
|
|
436
|
+
inputSchema: truthCertificationInputSchema,
|
|
437
|
+
},
|
|
438
|
+
wrapTool(async (params) => {
|
|
439
|
+
const {
|
|
440
|
+
batchId,
|
|
441
|
+
idempotencyKey,
|
|
442
|
+
...body
|
|
443
|
+
} = truthCertificationSchema.parse(params);
|
|
444
|
+
return apiRequest(
|
|
445
|
+
'POST',
|
|
446
|
+
`/api/jobscout/apply/batches/${batchId}/truth-certification`,
|
|
447
|
+
body,
|
|
448
|
+
false,
|
|
449
|
+
false,
|
|
450
|
+
MCP_USER_AGENT,
|
|
451
|
+
{ 'Idempotency-Key': idempotencyKey }
|
|
452
|
+
);
|
|
453
|
+
}, 'Failed to certify batch truthfulness')
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
server.registerTool(
|
|
457
|
+
'trackly_start_apply_run',
|
|
458
|
+
{
|
|
459
|
+
description: 'Start a legacy single run, or start/recover a frozen member when the complete batch binding is supplied. Recovered members already carrying runId must reuse that run without calling this tool.',
|
|
460
|
+
inputSchema: startApplyRunInputSchema,
|
|
461
|
+
},
|
|
462
|
+
wrapTool(async (params) => apiRequest(
|
|
463
|
+
'POST',
|
|
464
|
+
'/api/jobscout/apply/runs',
|
|
465
|
+
startApplyRunSchema.parse(params),
|
|
466
|
+
false,
|
|
467
|
+
false,
|
|
468
|
+
MCP_USER_AGENT
|
|
469
|
+
), 'Failed to start apply run')
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
server.tool(
|
|
473
|
+
'trackly_get_apply_evidence',
|
|
474
|
+
'Get the authenticated user\'s aggregate, value-free Apply beta evidence and release gate. The report never returns answers, contact values, addresses, or page text.',
|
|
475
|
+
{
|
|
476
|
+
windowDays: z.number().int().min(1).max(365).optional(),
|
|
477
|
+
targetReviewedRuns: z.number().int().min(1).max(1000).optional(),
|
|
478
|
+
},
|
|
479
|
+
wrapTool(async ({ windowDays, targetReviewedRuns }) => {
|
|
480
|
+
const qs = new URLSearchParams();
|
|
481
|
+
if (windowDays !== undefined) qs.set('windowDays', String(windowDays));
|
|
482
|
+
if (targetReviewedRuns !== undefined) qs.set('targetReviewedRuns', String(targetReviewedRuns));
|
|
483
|
+
const query = qs.toString();
|
|
484
|
+
const suffix = query ? `?${query}` : '';
|
|
485
|
+
return apiRequest('GET', `/api/jobscout/apply/evidence${suffix}`, null, false, false, MCP_USER_AGENT);
|
|
486
|
+
}, 'Failed to fetch apply evidence')
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
server.tool(
|
|
490
|
+
'trackly_get_apply_protocol',
|
|
491
|
+
'Get the current browser workflow, ATS support matrix, integrity rules, and compatible public-skill major version. Fetch at the start of every run and again after maintenance before resuming the existing run.',
|
|
492
|
+
{},
|
|
493
|
+
wrapTool(async () => apiRequest('GET', '/api/jobscout/apply/protocol', null, false, false, MCP_USER_AGENT), 'Failed to fetch apply protocol')
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
server.tool(
|
|
497
|
+
'trackly_report_apply_observation',
|
|
498
|
+
'Report a redacted ATS mechanics or scenario-coverage observation. Never include answer values, addresses, contact data, OTPs, or free-form page content.',
|
|
499
|
+
{
|
|
500
|
+
runId: z.number().int().min(1),
|
|
501
|
+
batchId: z.number().int().min(1).optional(),
|
|
502
|
+
memberId: z.number().int().min(1).optional(),
|
|
503
|
+
inspectionEpoch: z.number().int().min(0).optional(),
|
|
504
|
+
leaseToken: z.string().min(1).max(1024).optional(),
|
|
505
|
+
provider: z.string().regex(SAFE_OBSERVATION_CODE),
|
|
506
|
+
fieldLabel: z.string().min(1).max(1000),
|
|
507
|
+
observationType: z.string().regex(SAFE_OBSERVATION_CODE),
|
|
508
|
+
resolutionCode: z.string().regex(SAFE_OBSERVATION_CODE).optional(),
|
|
509
|
+
metadata: z.object({
|
|
510
|
+
controlType: z.string().regex(SAFE_OBSERVATION_CODE).optional(),
|
|
511
|
+
required: z.boolean().optional(),
|
|
512
|
+
errorCode: z.string().regex(SAFE_OBSERVATION_CODE).optional(),
|
|
513
|
+
committed: z.boolean(),
|
|
514
|
+
scenarioCode: z.enum(APPLY_SCENARIO_CODES),
|
|
515
|
+
browserSurface: z.enum(APPLY_BROWSER_SURFACES),
|
|
516
|
+
browserBindingHash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
517
|
+
resumedAfterHandoff: z.boolean().optional(),
|
|
518
|
+
}),
|
|
519
|
+
},
|
|
520
|
+
wrapTool(async (params) => apiRequest('POST', '/api/jobscout/apply/observations', params, false, false, MCP_USER_AGENT), 'Failed to report apply observation')
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
server.tool(
|
|
524
|
+
'trackly_report_apply_observations',
|
|
525
|
+
'Bulk-report up to 20 redacted, batch-bound ATS mechanics or scenario-coverage observations in one request. Never include answer values, addresses, contact data, OTPs, or page content.',
|
|
526
|
+
{
|
|
527
|
+
observations: z.array(z.object({
|
|
528
|
+
runId: z.number().int().min(1),
|
|
529
|
+
batchId: z.number().int().min(1),
|
|
530
|
+
memberId: z.number().int().min(1),
|
|
531
|
+
inspectionEpoch: z.number().int().min(0),
|
|
532
|
+
leaseToken: z.string().min(1).max(1024),
|
|
533
|
+
provider: z.string().regex(SAFE_OBSERVATION_CODE),
|
|
534
|
+
fieldLabel: z.string().min(1).max(1000),
|
|
535
|
+
observationType: z.string().regex(SAFE_OBSERVATION_CODE),
|
|
536
|
+
resolutionCode: z.string().regex(SAFE_OBSERVATION_CODE).optional(),
|
|
537
|
+
metadata: z.object({
|
|
538
|
+
controlType: z.string().regex(SAFE_OBSERVATION_CODE).optional(),
|
|
539
|
+
required: z.boolean().optional(),
|
|
540
|
+
errorCode: z.string().regex(SAFE_OBSERVATION_CODE).optional(),
|
|
541
|
+
committed: z.boolean(),
|
|
542
|
+
scenarioCode: z.enum(APPLY_SCENARIO_CODES),
|
|
543
|
+
browserSurface: z.enum(APPLY_BROWSER_SURFACES),
|
|
544
|
+
browserBindingHash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
545
|
+
resumedAfterHandoff: z.boolean().optional(),
|
|
546
|
+
}),
|
|
547
|
+
})).min(1).max(APPLY_BATCH_MAX_BULK_MUTATIONS),
|
|
548
|
+
},
|
|
549
|
+
wrapTool(
|
|
550
|
+
async (params) => apiRequest(
|
|
551
|
+
'POST',
|
|
552
|
+
'/api/jobscout/apply/observations/bulk',
|
|
553
|
+
params,
|
|
554
|
+
false,
|
|
555
|
+
false,
|
|
556
|
+
MCP_USER_AGENT
|
|
557
|
+
),
|
|
558
|
+
'Failed to report bulk apply observations'
|
|
559
|
+
)
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
server.tool(
|
|
563
|
+
'trackly_record_application_outcome',
|
|
564
|
+
'Record review readiness or a user-confirmed outcome. Mark submitted only after a success page or explicit user confirmation.',
|
|
565
|
+
{
|
|
566
|
+
runId: z.number().int().min(1),
|
|
567
|
+
batchId: z.number().int().min(1).optional(),
|
|
568
|
+
memberId: z.number().int().min(1).optional(),
|
|
569
|
+
inspectionEpoch: z.number().int().min(0).optional(),
|
|
570
|
+
leaseToken: z.string().min(1).max(1024).optional(),
|
|
571
|
+
outcome: z.enum(['review_ready', 'submitted', 'failed', 'blocked']),
|
|
572
|
+
confirmation: z.enum(['user_confirmation', 'success_page']).optional(),
|
|
573
|
+
},
|
|
574
|
+
wrapTool(async ({ runId, ...body }) => apiRequest('POST', `/api/jobscout/apply/runs/${runId}/outcome`, body, false, false, MCP_USER_AGENT), 'Failed to record application outcome')
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
server.tool(
|
|
578
|
+
'trackly_record_application_outcomes',
|
|
579
|
+
'Bulk-record up to 20 leased, batch-bound review or user-confirmed outcomes. Each member returns recorded or conflict without hiding sibling results.',
|
|
580
|
+
{
|
|
581
|
+
outcomes: z.array(z.object({
|
|
582
|
+
runId: z.number().int().min(1),
|
|
583
|
+
batchId: z.number().int().min(1),
|
|
584
|
+
memberId: z.number().int().min(1),
|
|
585
|
+
inspectionEpoch: z.number().int().min(0),
|
|
586
|
+
leaseToken: z.string().min(1).max(1024),
|
|
587
|
+
outcome: z.enum(['review_ready', 'submitted', 'failed', 'blocked']),
|
|
588
|
+
confirmation: z.enum(['user_confirmation', 'success_page']).optional(),
|
|
589
|
+
})).min(1).max(APPLY_BATCH_MAX_BULK_MUTATIONS),
|
|
590
|
+
},
|
|
591
|
+
wrapTool(
|
|
592
|
+
async (params) => apiRequest(
|
|
593
|
+
'POST',
|
|
594
|
+
'/api/jobscout/apply/outcomes/bulk',
|
|
595
|
+
params,
|
|
596
|
+
false,
|
|
597
|
+
false,
|
|
598
|
+
MCP_USER_AGENT
|
|
599
|
+
),
|
|
600
|
+
'Failed to record bulk application outcomes'
|
|
601
|
+
)
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
server.tool(
|
|
605
|
+
'trackly_prepare_resume',
|
|
606
|
+
'Download the authenticated default resume into a mode-0600 temporary Trackly cache and return exact-file proof for user confirmation before browser upload.',
|
|
607
|
+
{
|
|
608
|
+
runId: z.number().int().min(1),
|
|
609
|
+
browserSurface: z.enum(APPLY_BROWSER_SURFACES),
|
|
610
|
+
browserBindingHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
611
|
+
},
|
|
612
|
+
wrapTool(async ({ runId, browserSurface, browserBindingHash }) =>
|
|
613
|
+
prepareResume(runId, browserSurface, browserBindingHash), 'Failed to prepare default resume')
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
server.tool(
|
|
617
|
+
'trackly_verify_prepared_resume',
|
|
618
|
+
'Immediately before attachment, recompute the prepared resume fingerprint, validate its run and expiration, and lock the confirmed file read-only.',
|
|
619
|
+
{
|
|
620
|
+
runId: z.number().int().min(1),
|
|
621
|
+
resumeId: z.number().int().min(1),
|
|
622
|
+
confirmationId: z.string().min(1).max(200),
|
|
623
|
+
exactLocalPath: z.string().min(1).max(4096),
|
|
624
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/i),
|
|
625
|
+
sizeBytes: z.number().int().min(1),
|
|
626
|
+
expiresAt: z.string().datetime(),
|
|
627
|
+
},
|
|
628
|
+
wrapTool(async (proof) => verifyPreparedResume(proof), 'Prepared resume integrity verification failed')
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
server.registerPrompt('trackly-apply', {
|
|
632
|
+
title: 'Apply to the next Trackly job',
|
|
633
|
+
description: 'Run the manual-submit Trackly Apply workflow for the next user-approved job.',
|
|
634
|
+
}, async () => ({
|
|
635
|
+
messages: [{
|
|
636
|
+
role: 'user',
|
|
637
|
+
content: {
|
|
638
|
+
type: 'text',
|
|
639
|
+
text: 'Compatibility and batch gate: require Trackly Apply protocol 3.3.1 or newer and skill 4.2.2 or newer for every new frozen batch. Protocol 3.2 remains valid for the explicit legacy single-run workflow. Recover the active frozen batch before creating another, including for a one-job request. Do not fetch or select from the queue until active-batch recovery proves that no active batch exists; any later generic queue-first instruction applies only to the legacy 3.2 single-run workflow. Claim its lease, keep membership/order fixed, inspect all members before asking one grouped packet of questions, bind each initial or recovered browser surface to the same run and exact backend URL, and discard older-epoch evidence. If a bound start returns a transport failure, a non-access HTTP 5xx response, or an error explicitly marked retryable true, preserve the frozen member and browser state, refetch the same active batch, renew its lease, and retry the same complete binding exactly once. Classify the retry response independently with the same rules: route maintenance_mode or planned_maintenance from either attempt through maintenance recovery, surface controlled-access/request errors marked retryable false and every other HTTP 4xx response unchanged, and only classify a second transport failure, non-access HTTP 5xx response, or explicitly retryable error as backend_run_start_unavailable. Never relabel a permanent retry response as an outage. Preserve the unchanged frozen member as the durable resume point, continue siblings after backend_run_start_unavailable, and never checkpoint the pre-run failure or detach it into an unbound legacy run. Require one exact batch resume approval plus immediate local proof before each attachment; ordinary member-version checkpoints do not revoke unchanged resume-content approval. If no form in a truth-certified subset exposes a resume control, certify truth with resumeDependency not_applicable and no resume identity. After durable review-ready checkpoints, truth-certify and hand off the exact complete subset that is currently review-ready without waiting for needs-input members. Keep unresolved members frozen and resumable; when another member becomes ready later, create a fresh certification for the then-current complete review-ready subset. After manual Submit, keep submission request, success-page or explicit user-confirmation, provider receipt, and three-part surface-close proof separate and redacted. Treat submission reconciliation as a durable commit gate: keep the confirmation tab open until a refetch proves member lifecycle submitted and Trackly job state applied_confirmed.',
|
|
640
|
+
},
|
|
641
|
+
}, {
|
|
642
|
+
role: 'user',
|
|
643
|
+
content: {
|
|
644
|
+
type: 'text',
|
|
645
|
+
text: 'Fetch the Trackly Apply protocol, profile onboarding, profile, and approved queue. Resolve missing answers with me. Treat required completeness separately from optional reusable coverage and employer-specific contextual questions. Before starting anything, stop on every non-null executionBlocker and every manual_only item. Start only the selected approved queue item, require major(run.protocolVersion) === major(protocol.version), require protocol.compatibleSkillMajor === 4, preserve the stored version for a resumed run, and require its provider, atsCapability, required scenarios, and originPolicy to match the queue preflight. Reclaim semantic browser control, verify the exact job/run/tab binding, hash that value-free binding, and report the same-run browser_ready attestation with committed=true. Before entering private data, require the visible company and role to match the run binding and, when available, the requisition identifier to match the stored job URL. When job_identity_match is required, report a value-free committed scenario_coverage attestation only after that visible identity check passes; never include the company, role, URL, requisition identifier, page text, or any profile value in that observation. On exact-origin fallback, revalidate the frozen company, role, and available requisition identity after every navigation or redirect and before entering any additional private data. Normalize every page, redirect, and data-receiving iframe URL; accept an exact authorized origin or hostname only when host === allowedDomain or host.endsWith("." + allowedDomain), never by substring or page text. When originPolicy.verification is trackly_employer_source_exact_origin, authorize only the exact origin in authorizedOrigins: never promote it to a host suffix and never carry it across a redirect or iframe origin change. For every other vendor-hosted ATS policy, require both originPolicy.tenantRule and originPolicy.verifiedAtsTenant to be non-null or stop before private data entry. Execute the backend-owned originPolicy.tenantRule exactly after every redirect or data-receiving iframe change, including its extraction, exact-host-depth, locale, percent-decoding, normalization, and fail-closed semantics, then require the normalized result to equal originPolicy.verifiedAtsTenant; never invent or reinterpret a strategy token. Obey every capability stop condition. Determine whether the form has a semantically identified Resume or CV attachment control. Only when that specific control exists, prepare the run-bound resume locally with that browser surface and binding hash, show me its exact path, filename, size, SHA-256, run, and expiration, and obtain my explicit confirmation. Treat cover-letter, portfolio, transcript, and other supporting-document controls separately according to the profile and protocol; never upload a resume to them. Immediately before attaching the resume, use the local verifier to validate the signed proof, recompute hash and size, check expiration, and lock the file read-only. Fill every visible field whose answer is already known, including optional fields, before asking one grouped packet for the remaining unknowns. Use real semantic UI actions and the provider playbook for Greenhouse, Ashby, HiBob, or the active capability; after every select, radio, checkbox, masked input, and upload, verify the committed DOM or accessibility state and that any related required error disappeared. Then sweep all required fields, duplicate contact values, correction banners, and the final consent control. Report a same-run passed or corrected scenario_coverage observation with committed=true for every backend-required scenario except browser_reclaim, which is satisfied only by browser_ready with the binding hash. Before every review_ready outcome, also report value-free committed critical_contact_integrity and manual_submit_boundary evidence; never include contact values, answers, page text, or local paths. If a required or universal review scenario cannot pass, record blocked rather than review_ready. Stop before Submit. If maintenance interrupts the run, retain the run and browser context, wait for the advertised window, refetch protocol, queue, and profile state, and resume the existing agent_browser run. Never start a duplicate run, blindly retry a mutation, enter credentials or verification codes, evade human verification, or click Submit.',
|
|
646
|
+
},
|
|
647
|
+
}],
|
|
648
|
+
}));
|
|
649
|
+
|
|
650
|
+
server.registerResource('trackly-apply-protocol', 'trackly://apply/protocol', {
|
|
651
|
+
title: 'Current Trackly Apply protocol',
|
|
652
|
+
description: 'Versioned browser mechanics and compatibility contract.',
|
|
653
|
+
mimeType: 'application/json',
|
|
654
|
+
}, async (uri) => {
|
|
655
|
+
try {
|
|
656
|
+
const result = await apiRequest('GET', '/api/jobscout/apply/protocol', null, false, false, MCP_USER_AGENT);
|
|
657
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result) }] };
|
|
658
|
+
} catch (error) {
|
|
659
|
+
return throwMcpResourceError(error);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
module.exports = {
|
|
665
|
+
registerApplyTools,
|
|
666
|
+
};
|