Skip to content

Commit 2793067

Browse files
Bonusly search by company, participations and reward redemption integrations with MCP optimized (#21701)
1 parent bfe7ca8 commit 2793067

12 files changed

Lines changed: 786 additions & 7 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// x-pd-ai: optimized
2+
import bonusly from "../../bonusly.app.mjs";
3+
4+
export default {
5+
key: "bonusly-get-company-participation-report",
6+
name: "Get Company Participation Report",
7+
description: "Return company-level recognition participation analytics - giving/receiving rates by group (department, location, etc.) or by manager and team, for a given date range. [See the documentation](https://docs.bonus.ly/reference/adminparticipationreport-1)",
8+
version: "0.0.1",
9+
type: "action",
10+
annotations: {
11+
readOnlyHint: true,
12+
destructiveHint: false,
13+
openWorldHint: true,
14+
},
15+
props: {
16+
bonusly,
17+
reportView: {
18+
type: "string",
19+
label: "Report View",
20+
description: "Which participation view to return.",
21+
options: [
22+
"giving_and_receiving",
23+
"managers_and_teams",
24+
],
25+
},
26+
startDate: {
27+
propDefinition: [
28+
bonusly,
29+
"startDate",
30+
],
31+
description: "Start of the report's date range, in `YYYY-MM-DD` format, e.g. `2026-01-01`.",
32+
optional: false,
33+
},
34+
endDate: {
35+
propDefinition: [
36+
bonusly,
37+
"endDate",
38+
],
39+
description: "End of the report's date range, in `YYYY-MM-DD` format, e.g. `2026-06-30`. Bonusly cannot report on the current or an ongoing month, so this must be no later than the last day of the previous month - for example, any time during August 2026 the latest valid value is `2026-07-31`. A later date is rejected.",
40+
optional: false,
41+
},
42+
customPropertyGroup: {
43+
type: "string",
44+
label: "Custom Property Group",
45+
description: "Grouping dimension to break the `giving_and_receiving` view down by, e.g. `department` or `location`. Ignored for the `managers_and_teams` view.",
46+
optional: true,
47+
},
48+
includeTrend: {
49+
type: "boolean",
50+
label: "Include Trend",
51+
description: "Set to `true` to include trend data alongside the current period's metrics.",
52+
optional: true,
53+
},
54+
},
55+
async run({ $ }) {
56+
const response = await this.bonusly.getAdminParticipationReport({
57+
$,
58+
reportView: this.reportView,
59+
startDate: this.startDate,
60+
endDate: this.endDate,
61+
customPropertyGroup: this.customPropertyGroup,
62+
includeTrend: this.includeTrend,
63+
});
64+
65+
$.export("$summary", `Retrieved ${this.reportView} participation report for ${this.startDate} to ${this.endDate}`);
66+
return response;
67+
},
68+
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// x-pd-ai: optimized
2+
import bonusly from "../../bonusly.app.mjs";
3+
4+
export default {
5+
key: "bonusly-give-bonus",
6+
name: "Give Bonus",
7+
description: "Send recognition (a bonus) to one or more colleagues on behalf of the authenticated caller. Do not include `@mentions` or the point amount in the `reason` yourself - Bonusly synthesizes those from `recipients` and `amount` automatically. [See the documentation](https://docs.bonus.ly/reference/giverecognition-1)",
8+
version: "0.0.1",
9+
type: "action",
10+
annotations: {
11+
readOnlyHint: false,
12+
destructiveHint: false,
13+
openWorldHint: true,
14+
},
15+
props: {
16+
bonusly,
17+
recipients: {
18+
type: "string[]",
19+
label: "Recipients",
20+
description: "One or more recipients - each entry can be a user ID, an email address, or a display name, e.g. `john.smith@company.com`. Use **Search Users** to look up one person by name, or **List Users In Department**, **List Users In Location**, or **List Top-Level Users** to recognize a whole roster at once.",
21+
},
22+
amount: {
23+
type: "integer",
24+
label: "Amount",
25+
description: "Points to give. Pass `0` only if your company allows zero-point recognition.",
26+
min: 0,
27+
},
28+
reason: {
29+
type: "string",
30+
label: "Reason",
31+
description: "Free-form recognition message explaining why you're giving this bonus, e.g. `Great work on the product launch!`.",
32+
},
33+
hashtag: {
34+
type: "string",
35+
label: "Hashtag",
36+
description: "One company hashtag, without the leading `#`, e.g. `teamwork`. Some Bonusly companies require a hashtag on every recognition - if the tool rejects the request for a missing hashtag, set one here.",
37+
optional: true,
38+
},
39+
},
40+
async run({ $ }) {
41+
const response = await this.bonusly.giveRecognition({
42+
$,
43+
recipients: this.recipients,
44+
amount: this.amount,
45+
reason: this.reason,
46+
hashtag: this.hashtag,
47+
});
48+
49+
const names = response.recipients?.map(({ name }) => name).join(", ");
50+
const bonusId = response.bonus_id
51+
? ` (bonus ID: ${response.bonus_id})`
52+
: "";
53+
$.export("$summary", `Gave ${this.amount} points to ${names || "recipient(s)"}${bonusId}`);
54+
return response;
55+
},
56+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// x-pd-ai: optimized
2+
import bonusly from "../../bonusly.app.mjs";
3+
4+
export default {
5+
key: "bonusly-list-departments",
6+
name: "List Departments",
7+
description: "List the distinct departments configured for users in the authenticated caller's company, with a user count for each. Call this first to discover the exact department names accepted by **List Users In Department**, which matches exactly and returns nothing for a misspelled or differently-cased name. [See the documentation](https://docs.bonus.ly/reference/listdepartments)",
8+
version: "0.0.1",
9+
type: "action",
10+
annotations: {
11+
readOnlyHint: true,
12+
destructiveHint: false,
13+
openWorldHint: true,
14+
},
15+
props: {
16+
bonusly,
17+
search: {
18+
propDefinition: [
19+
bonusly,
20+
"search",
21+
],
22+
description: "Narrow the results to departments whose name starts with this text, e.g. `Eng` matches `Engineering`. This is a prefix match, so it will not match text in the middle of a name. Omit to return every department.",
23+
},
24+
pageSize: {
25+
propDefinition: [
26+
bonusly,
27+
"pageSize",
28+
],
29+
description: "Maximum number of departments to return in this page. Defaults to Bonusly's standard page size if omitted.",
30+
},
31+
cursor: {
32+
propDefinition: [
33+
bonusly,
34+
"cursor",
35+
],
36+
},
37+
},
38+
async run({ $ }) {
39+
const response = await this.bonusly.listDepartments({
40+
$,
41+
search: this.search,
42+
pageSize: this.pageSize,
43+
cursor: this.cursor,
44+
});
45+
46+
const morePages = response.next_cursor
47+
? " (more pages available)"
48+
: "";
49+
$.export("$summary", `Found ${response.departments?.length ?? 0} department(s)${morePages}`);
50+
return response;
51+
},
52+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// x-pd-ai: optimized
2+
import bonusly from "../../bonusly.app.mjs";
3+
4+
export default {
5+
key: "bonusly-list-locations",
6+
name: "List Locations",
7+
description: "List the distinct locations configured for users in the authenticated caller's company, with a user count for each. Call this first to discover the exact location names accepted by **List Users In Location**, which matches exactly and returns nothing for a misspelled or differently-cased name. [See the documentation](https://docs.bonus.ly/reference/listlocations)",
8+
version: "0.0.1",
9+
type: "action",
10+
annotations: {
11+
readOnlyHint: true,
12+
destructiveHint: false,
13+
openWorldHint: true,
14+
},
15+
props: {
16+
bonusly,
17+
search: {
18+
propDefinition: [
19+
bonusly,
20+
"search",
21+
],
22+
description: "Narrow the results to locations whose name starts with this text, e.g. `Ber` matches `Berlin`. This is a prefix match, so it will not match text in the middle of a name. Omit to return every location.",
23+
},
24+
pageSize: {
25+
propDefinition: [
26+
bonusly,
27+
"pageSize",
28+
],
29+
description: "Maximum number of locations to return in this page. Defaults to Bonusly's standard page size if omitted.",
30+
},
31+
cursor: {
32+
propDefinition: [
33+
bonusly,
34+
"cursor",
35+
],
36+
},
37+
},
38+
async run({ $ }) {
39+
const response = await this.bonusly.listLocations({
40+
$,
41+
search: this.search,
42+
pageSize: this.pageSize,
43+
cursor: this.cursor,
44+
});
45+
46+
const morePages = response.next_cursor
47+
? " (more pages available)"
48+
: "";
49+
$.export("$summary", `Found ${response.locations?.length ?? 0} location(s)${morePages}`);
50+
return response;
51+
},
52+
};
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// x-pd-ai: optimized
2+
import bonusly from "../../bonusly.app.mjs";
3+
4+
export default {
5+
key: "bonusly-list-reward-redemptions",
6+
name: "List Reward Redemptions",
7+
description: "Return paginated reward redemption records for the caller's company, with optional filtering by user email, date range, and fulfillment status. This is a company-wide admin report, not just the caller's own redemptions. [See the documentation](https://docs.bonus.ly/reference/adminrewardsredemptionsreport-1)",
8+
version: "0.0.1",
9+
type: "action",
10+
annotations: {
11+
readOnlyHint: true,
12+
destructiveHint: false,
13+
openWorldHint: true,
14+
},
15+
props: {
16+
bonusly,
17+
userEmail: {
18+
type: "string",
19+
label: "User Email",
20+
description: "Filter to redemptions made by this user's email address, e.g. `john.smith@company.com`.",
21+
optional: true,
22+
},
23+
startDate: {
24+
propDefinition: [
25+
bonusly,
26+
"startDate",
27+
],
28+
description: "Only include redemptions on or after this date, e.g. `2026-01-01`.",
29+
},
30+
endDate: {
31+
propDefinition: [
32+
bonusly,
33+
"endDate",
34+
],
35+
description: "Only include redemptions on or before this date, e.g. `2026-06-30`.",
36+
},
37+
unfulfilled: {
38+
type: "boolean",
39+
label: "Unfulfilled Only",
40+
description: "Set to `true` to return only redemptions that haven't been fulfilled yet.",
41+
optional: true,
42+
},
43+
aasmState: {
44+
type: "string",
45+
label: "State",
46+
description: "Filter by the redemption's internal state (e.g. `approved`, `pending`, `declined`). Bonusly's docs don't publish the full list of accepted values, so confirm against a real redemption's `aasm_state` field if this filter returns no results. Leave blank to include redemptions in any state.",
47+
optional: true,
48+
},
49+
range: {
50+
type: "string",
51+
label: "Range",
52+
description: "A predefined date range shortcut, if supported by your Bonusly plan (e.g. `this_month`). `Start Date`/`End Date` take precedence when both are set.",
53+
optional: true,
54+
},
55+
page: {
56+
type: "integer",
57+
label: "Page",
58+
description: "Page number to fetch, starting at `1`.",
59+
min: 1,
60+
optional: true,
61+
},
62+
perPage: {
63+
type: "integer",
64+
label: "Per Page",
65+
description: "Number of redemptions to return per page.",
66+
min: 1,
67+
max: 100,
68+
optional: true,
69+
},
70+
sort: {
71+
type: "string",
72+
label: "Sort Field",
73+
description: "Field to sort results by, e.g. `created_at`.",
74+
optional: true,
75+
},
76+
direction: {
77+
type: "string",
78+
label: "Sort Direction",
79+
description: "Sort direction for the `Sort Field`, e.g. `asc` or `desc`.",
80+
optional: true,
81+
},
82+
},
83+
async run({ $ }) {
84+
const response = await this.bonusly.getAdminRewardsRedemptionsReport({
85+
$,
86+
userEmail: this.userEmail,
87+
startDate: this.startDate,
88+
endDate: this.endDate,
89+
unfulfilled: this.unfulfilled,
90+
aasmState: this.aasmState,
91+
range: this.range,
92+
page: this.page,
93+
perPage: this.perPage,
94+
sort: this.sort,
95+
direction: this.direction,
96+
});
97+
98+
$.export("$summary", `Found ${response.pagination?.total_count ?? 0} reward redemption(s)`);
99+
return response;
100+
},
101+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// x-pd-ai: optimized
2+
import bonusly from "../../bonusly.app.mjs";
3+
4+
export default {
5+
key: "bonusly-list-top-level-users",
6+
name: "List Top-Level Users",
7+
description: "List the users in the authenticated caller's company who have no manager - the roots of the org chart, typically executives. Use this to answer who sits at the top of the company, or as a starting point when you have no other identifier to work from. Takes no required input. [See the documentation](https://docs.bonus.ly/reference/listtoplevelusers)",
8+
version: "0.0.1",
9+
type: "action",
10+
annotations: {
11+
readOnlyHint: true,
12+
destructiveHint: false,
13+
openWorldHint: true,
14+
},
15+
props: {
16+
bonusly,
17+
search: {
18+
propDefinition: [
19+
bonusly,
20+
"search",
21+
],
22+
description: "Narrow the results by matching this text against user names or emails, e.g. `john` or `john.smith@company.com`. Omit to return every top-level user.",
23+
},
24+
pageSize: {
25+
propDefinition: [
26+
bonusly,
27+
"pageSize",
28+
],
29+
description: "Maximum number of users to return in this page. Defaults to Bonusly's standard page size if omitted.",
30+
},
31+
cursor: {
32+
propDefinition: [
33+
bonusly,
34+
"cursor",
35+
],
36+
},
37+
},
38+
async run({ $ }) {
39+
const response = await this.bonusly.listTopLevelUsers({
40+
$,
41+
search: this.search,
42+
pageSize: this.pageSize,
43+
cursor: this.cursor,
44+
});
45+
46+
const morePages = response.next_cursor
47+
? " (more pages available)"
48+
: "";
49+
$.export("$summary", `Found ${response.users?.length ?? 0} top-level user(s)${morePages}`);
50+
return response;
51+
},
52+
};

0 commit comments

Comments
 (0)