Skip to content

Commit cf906a8

Browse files
committed
feat(gupy): list candidate professional experiences for a job
- add fields param to list_applications (name|id|code|all) - add list_application_experiences tool that queries applications with fields=all and returns a clean per-candidate work experience summary
1 parent 7d8d122 commit cf906a8

3 files changed

Lines changed: 111 additions & 1 deletion

File tree

packages/gupy/src/server.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
GetJobParamsSchema,
99
UpdateJobStatusParamsSchema,
1010
ListApplicationsParamsSchema,
11+
ListApplicationExperiencesParamsSchema,
1112
MoveApplicationParamsSchema,
1213
CreateApplicationCommentParamsSchema,
1314
ListApplicationCommentsParamsSchema,
@@ -100,7 +101,7 @@ export class GupyMCPServer {
100101
{
101102
title: "List Applications",
102103
description:
103-
"List candidate applications for a specific job, with optional step/status filters",
104+
"List candidate applications for a specific job, with optional step/status filters. Use fields='all' to include candidate details such as work experience, education and languages.",
104105
inputSchema: ListApplicationsParamsSchema.shape,
105106
},
106107
async (params) => {
@@ -110,6 +111,21 @@ export class GupyMCPServer {
110111
}
111112
);
112113

114+
this.server.registerTool(
115+
"list_application_experiences",
116+
{
117+
title: "List Application Professional Experiences",
118+
description:
119+
"List the professional (work) experiences of candidates who applied to a specific job. Returns each candidate's name, email, schooling and a clean list of work experiences (role, company, activities, period). Internally queries the applications endpoint with fields=all.",
120+
inputSchema: ListApplicationExperiencesParamsSchema.shape,
121+
},
122+
async (params) => {
123+
return this.tools.listApplicationExperiences(
124+
ListApplicationExperiencesParamsSchema.parse(params)
125+
);
126+
}
127+
);
128+
113129
this.server.registerTool(
114130
"move_application",
115131
{

packages/gupy/src/tools.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
GetJobParams,
66
UpdateJobStatusParams,
77
ListApplicationsParams,
8+
ListApplicationExperiencesParams,
89
MoveApplicationParams,
910
CreateApplicationCommentParams,
1011
ListApplicationCommentsParams,
@@ -92,6 +93,7 @@ export class GupyMCPTools {
9293
offset: params.offset,
9394
currentStep: params.currentStep,
9495
status: params.status,
96+
fields: params.fields,
9597
});
9698
const data = await this.client.request<unknown>(
9799
"GET",
@@ -105,6 +107,29 @@ export class GupyMCPTools {
105107
}
106108
}
107109

110+
async listApplicationExperiences(
111+
params: ListApplicationExperiencesParams
112+
): Promise<McpToolResult> {
113+
try {
114+
const query = this.cleanQuery({
115+
limit: params.limit,
116+
offset: params.offset,
117+
currentStep: params.currentStep,
118+
status: params.status,
119+
fields: "all",
120+
});
121+
const data = await this.client.request<unknown>(
122+
"GET",
123+
`/api/v1/jobs/${params.jobId}/applications`,
124+
undefined,
125+
query
126+
);
127+
return this.ok(this.extractExperiences(data));
128+
} catch (error) {
129+
return this.formatError(error);
130+
}
131+
}
132+
108133
async moveApplication(
109134
params: MoveApplicationParams
110135
): Promise<McpToolResult> {
@@ -260,6 +285,53 @@ export class GupyMCPTools {
260285
}
261286
}
262287

288+
private extractExperiences(data: unknown): unknown {
289+
const root = data as Record<string, unknown> | null;
290+
const list = Array.isArray(root?.data)
291+
? (root?.data as unknown[])
292+
: Array.isArray(data)
293+
? (data as unknown[])
294+
: [];
295+
296+
const applications = list.map((item) => {
297+
const application = item as Record<string, unknown>;
298+
const candidate =
299+
(application.candidate as Record<string, unknown> | undefined) ?? {};
300+
const rawExperiences = Array.isArray(candidate.workExperience)
301+
? (candidate.workExperience as Record<string, unknown>[])
302+
: [];
303+
304+
const workExperience = rawExperiences.map((experience) => ({
305+
role: experience.role ?? null,
306+
companyName: experience.companyName ?? null,
307+
activitiesPerformed: experience.activitiesPerformed ?? null,
308+
startMonth: experience.startMonth ?? null,
309+
startYear: experience.startYear ?? null,
310+
endMonth: experience.endMonth ?? null,
311+
endYear: experience.endYear ?? null,
312+
}));
313+
314+
return {
315+
applicationId: application.id ?? null,
316+
candidateName: candidate.name ?? null,
317+
candidateEmail: candidate.email ?? null,
318+
schooling: candidate.schooling ?? null,
319+
schoolingStatus: candidate.schoolingStatus ?? null,
320+
workExperience,
321+
};
322+
});
323+
324+
return {
325+
summary: {
326+
total: applications.length,
327+
withExperience: applications.filter(
328+
(application) => application.workExperience.length > 0
329+
).length,
330+
},
331+
applications,
332+
};
333+
}
334+
263335
private cleanQuery(
264336
obj: Record<string, string | number | boolean | undefined>
265337
): Record<string, string | number | boolean> {

packages/gupy/src/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,25 @@ export const ListApplicationsParamsSchema = z.object({
7272
.string()
7373
.optional()
7474
.describe("Filter by application status"),
75+
fields: z
76+
.enum(["name", "id", "code", "all"])
77+
.optional()
78+
.describe(
79+
"Controls which application fields are returned. Use 'all' to include candidate details such as workExperience, academicQualification and languages. Defaults to Gupy's standard response when omitted."
80+
),
81+
});
82+
83+
export const ListApplicationExperiencesParamsSchema = z.object({
84+
jobId: z.union([z.string(), z.number()]).describe("Job ID"),
85+
...Pagination,
86+
currentStep: z
87+
.string()
88+
.optional()
89+
.describe("Filter applications by current step name"),
90+
status: z
91+
.string()
92+
.optional()
93+
.describe("Filter by application status"),
7594
});
7695

7796
export const MoveApplicationParamsSchema = z.object({
@@ -161,6 +180,9 @@ export type UpdateJobStatusParams = z.infer<typeof UpdateJobStatusParamsSchema>;
161180
export type ListApplicationsParams = z.infer<
162181
typeof ListApplicationsParamsSchema
163182
>;
183+
export type ListApplicationExperiencesParams = z.infer<
184+
typeof ListApplicationExperiencesParamsSchema
185+
>;
164186
export type MoveApplicationParams = z.infer<typeof MoveApplicationParamsSchema>;
165187
export type CreateApplicationCommentParams = z.infer<
166188
typeof CreateApplicationCommentParamsSchema

0 commit comments

Comments
 (0)