payrolla-mcp 0.2.1
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 +123 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +766 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Payrolla MCP Server
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for Turkish payroll calculations and budget simulations. Enables LLMs to calculate payroll, simulate budgets, and compare what-if scenarios.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g payrolla-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly with npx:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx payrolla-mcp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
### Environment Variables
|
|
20
|
+
|
|
21
|
+
| Variable | Required | Description |
|
|
22
|
+
|----------|----------|-------------|
|
|
23
|
+
| `PAYROLLA_API_KEY` | Yes | API key for Payrolla service |
|
|
24
|
+
| `PAYROLLA_DEBUG` | No | Set to `true` for debug logging |
|
|
25
|
+
|
|
26
|
+
### Claude Desktop Configuration
|
|
27
|
+
|
|
28
|
+
Add to your `claude_desktop_config.json`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"payrolla": {
|
|
34
|
+
"command": "npx",
|
|
35
|
+
"args": ["payrolla-mcp"],
|
|
36
|
+
"env": {
|
|
37
|
+
"PAYROLLA_API_KEY": "pk_live_xxxxx"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Available Tools
|
|
45
|
+
|
|
46
|
+
### calculate_payroll
|
|
47
|
+
|
|
48
|
+
Calculate payroll for a single employee.
|
|
49
|
+
|
|
50
|
+
**Input:**
|
|
51
|
+
- `name` - Employee name
|
|
52
|
+
- `wage` - Wage amount
|
|
53
|
+
- `calculationType` - 'Gross' or 'Net'
|
|
54
|
+
- `year` - Calculation year
|
|
55
|
+
- `month` - Starting month (1-12)
|
|
56
|
+
- `periodCount` - Number of months (optional, default: 1)
|
|
57
|
+
- `ssiType` - SSI type: 'S4A', 'S4B', or 'S4C' (optional, default: 'S4A')
|
|
58
|
+
- `extraPayments` - Array of extra payments (optional)
|
|
59
|
+
- `customParams` - Custom global parameters (optional)
|
|
60
|
+
|
|
61
|
+
### calculate_bulk_payroll
|
|
62
|
+
|
|
63
|
+
Calculate payroll for multiple employees with shared parameters.
|
|
64
|
+
|
|
65
|
+
**Input:**
|
|
66
|
+
- `employees` - Array of employee objects
|
|
67
|
+
- `year` - Calculation year
|
|
68
|
+
- `month` - Starting month
|
|
69
|
+
- `periodCount` - Number of months (use 12 for yearly)
|
|
70
|
+
- `customParams` - Shared custom parameters (optional)
|
|
71
|
+
|
|
72
|
+
### simulate_budget
|
|
73
|
+
|
|
74
|
+
Simulate budget with what-if scenarios.
|
|
75
|
+
|
|
76
|
+
**Input:**
|
|
77
|
+
- `employees` - Array of employees
|
|
78
|
+
- `year` - Calculation year
|
|
79
|
+
- `periodCount` - Number of months
|
|
80
|
+
- `scenario` - Scenario configuration:
|
|
81
|
+
- `salaryRaisePercent` - Salary raise percentage
|
|
82
|
+
- `minWage` - Custom minimum wage
|
|
83
|
+
- `taxLimitIncreasePercent` - Tax bracket limit increase
|
|
84
|
+
- `customTaxBrackets` - Custom tax brackets
|
|
85
|
+
|
|
86
|
+
### compare_scenarios
|
|
87
|
+
|
|
88
|
+
Compare multiple budget scenarios side by side.
|
|
89
|
+
|
|
90
|
+
**Input:**
|
|
91
|
+
- `employees` - Array of employees
|
|
92
|
+
- `year` - Calculation year
|
|
93
|
+
- `periodCount` - Number of months
|
|
94
|
+
- `scenarios` - Array of scenario configurations
|
|
95
|
+
|
|
96
|
+
### get_default_params
|
|
97
|
+
|
|
98
|
+
Get default Turkish payroll parameters for a year.
|
|
99
|
+
|
|
100
|
+
**Input:**
|
|
101
|
+
- `year` - Year to get parameters for
|
|
102
|
+
|
|
103
|
+
## Available Prompts
|
|
104
|
+
|
|
105
|
+
### budget_simulation
|
|
106
|
+
|
|
107
|
+
Interactive prompt for simulating yearly payroll budget.
|
|
108
|
+
|
|
109
|
+
### salary_raise_analysis
|
|
110
|
+
|
|
111
|
+
Analyze the cost impact of different salary raise percentages.
|
|
112
|
+
|
|
113
|
+
### year_planning
|
|
114
|
+
|
|
115
|
+
Plan yearly payroll considering potential changes.
|
|
116
|
+
|
|
117
|
+
## Example Conversations
|
|
118
|
+
|
|
119
|
+
### Budget Simulation
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
User: I have 3 employees: Ali 35k net, Ayse 45k net, Mehmet 60k gross.
|
|
123
|
+
What's my yearly budget if I give 10% raise and minimum wage becomes 30k?
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/server.ts
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { PayrollaClient as PayrollaClient2 } from "payrolla";
|
|
9
|
+
import { z as z2 } from "zod";
|
|
10
|
+
|
|
11
|
+
// src/tools/calculate.ts
|
|
12
|
+
import {
|
|
13
|
+
SSIType,
|
|
14
|
+
CalculationType,
|
|
15
|
+
PaymentPeriodType,
|
|
16
|
+
PeriodLengthType,
|
|
17
|
+
PaymentType
|
|
18
|
+
} from "payrolla";
|
|
19
|
+
function mapSSIType(ssiType) {
|
|
20
|
+
switch (ssiType) {
|
|
21
|
+
case "S4B":
|
|
22
|
+
return SSIType.S4B;
|
|
23
|
+
case "S4C":
|
|
24
|
+
return SSIType.S4C;
|
|
25
|
+
case "S4A":
|
|
26
|
+
default:
|
|
27
|
+
return SSIType.S4A;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function mapCalculationType(calcType) {
|
|
31
|
+
return calcType === "Gross" ? CalculationType.Gross : CalculationType.Net;
|
|
32
|
+
}
|
|
33
|
+
function buildCustomGlobalParams(customParams) {
|
|
34
|
+
if (!customParams) return void 0;
|
|
35
|
+
return {
|
|
36
|
+
minWage: customParams.minWage,
|
|
37
|
+
minWageNet: customParams.minWageNet,
|
|
38
|
+
ssi_LowerLimit: customParams.ssiLowerLimit,
|
|
39
|
+
ssi_UpperLimit: customParams.ssiUpperLimit,
|
|
40
|
+
stampTaxRatio: customParams.stampTaxRatio,
|
|
41
|
+
incomeTaxLimits: customParams.incomeTaxLimits
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function calculatePayroll(client, input) {
|
|
45
|
+
const {
|
|
46
|
+
name,
|
|
47
|
+
wage,
|
|
48
|
+
calculationType,
|
|
49
|
+
ssiType,
|
|
50
|
+
year,
|
|
51
|
+
month,
|
|
52
|
+
periodCount = 1,
|
|
53
|
+
extraPayments,
|
|
54
|
+
customParams
|
|
55
|
+
} = input;
|
|
56
|
+
const payments = [
|
|
57
|
+
{
|
|
58
|
+
paymentAmount: 31,
|
|
59
|
+
paymentName: "Maas",
|
|
60
|
+
paymentType: PaymentType.RegularPayment,
|
|
61
|
+
paymentRef: "1"
|
|
62
|
+
}
|
|
63
|
+
];
|
|
64
|
+
if (extraPayments && extraPayments.length > 0) {
|
|
65
|
+
for (let i = 0; i < extraPayments.length; i++) {
|
|
66
|
+
const extra = extraPayments[i];
|
|
67
|
+
payments.push({
|
|
68
|
+
paymentAmount: extra.amount,
|
|
69
|
+
paymentName: extra.name,
|
|
70
|
+
paymentType: PaymentType.ExtraPay,
|
|
71
|
+
paymentRef: `extra_${i + 2}`,
|
|
72
|
+
calculationType: extra.type === "Net" ? CalculationType.Net : CalculationType.Gross
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const model = {
|
|
77
|
+
calcDate: `${year}-${String(month).padStart(2, "0")}-01`,
|
|
78
|
+
wageAmount: wage,
|
|
79
|
+
cumulativeIncomeTaxBase: 0,
|
|
80
|
+
cumulativeMinWageIncomeTaxBase: 0,
|
|
81
|
+
ssiType: mapSSIType(ssiType),
|
|
82
|
+
wageCalculationType: mapCalculationType(calculationType),
|
|
83
|
+
wagePeriodType: PaymentPeriodType.Monthly,
|
|
84
|
+
periodCount,
|
|
85
|
+
periodLengthType: PeriodLengthType.Month,
|
|
86
|
+
payments,
|
|
87
|
+
calculationParams: {
|
|
88
|
+
calculateMinWageExemption: true,
|
|
89
|
+
customGlobalParams: buildCustomGlobalParams(customParams)
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const result = await client.calculate(model);
|
|
93
|
+
let totalCost = 0;
|
|
94
|
+
let totalNet = 0;
|
|
95
|
+
let totalGross = 0;
|
|
96
|
+
const periods = [];
|
|
97
|
+
for (const payroll of result.payrolls) {
|
|
98
|
+
const pr = payroll.payrollResult;
|
|
99
|
+
totalCost += payroll.totalCost;
|
|
100
|
+
totalNet += pr.totalNet;
|
|
101
|
+
totalGross += pr.totalGross;
|
|
102
|
+
periods.push({
|
|
103
|
+
year: payroll.year,
|
|
104
|
+
month: payroll.month,
|
|
105
|
+
grossWage: pr.totalGross,
|
|
106
|
+
netWage: pr.totalNet,
|
|
107
|
+
employerCost: payroll.totalCost,
|
|
108
|
+
incomeTax: pr.totalIncomeTax,
|
|
109
|
+
stampTax: pr.totalStampTax,
|
|
110
|
+
employeeSSI: pr.totalSSIWorkerPrem,
|
|
111
|
+
employerSSI: pr.totalSSIEmployerPrem
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
employee: name,
|
|
116
|
+
totalCost,
|
|
117
|
+
totalNet,
|
|
118
|
+
totalGross,
|
|
119
|
+
periods
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function calculateBulkPayroll(client, input) {
|
|
123
|
+
const {
|
|
124
|
+
employees,
|
|
125
|
+
year,
|
|
126
|
+
month,
|
|
127
|
+
periodCount = 1,
|
|
128
|
+
customParams
|
|
129
|
+
} = input;
|
|
130
|
+
const employeeResults = [];
|
|
131
|
+
let totalYearlyCost = 0;
|
|
132
|
+
let totalYearlyNet = 0;
|
|
133
|
+
let totalYearlyGross = 0;
|
|
134
|
+
for (const emp of employees) {
|
|
135
|
+
const result = await calculatePayroll(client, {
|
|
136
|
+
name: emp.name,
|
|
137
|
+
wage: emp.wage,
|
|
138
|
+
calculationType: emp.calculationType,
|
|
139
|
+
ssiType: emp.ssiType,
|
|
140
|
+
year,
|
|
141
|
+
month,
|
|
142
|
+
periodCount,
|
|
143
|
+
extraPayments: emp.extraPayments,
|
|
144
|
+
customParams
|
|
145
|
+
});
|
|
146
|
+
employeeResults.push({
|
|
147
|
+
name: result.employee,
|
|
148
|
+
totalCost: result.totalCost,
|
|
149
|
+
totalNet: result.totalNet,
|
|
150
|
+
totalGross: result.totalGross
|
|
151
|
+
});
|
|
152
|
+
totalYearlyCost += result.totalCost;
|
|
153
|
+
totalYearlyNet += result.totalNet;
|
|
154
|
+
totalYearlyGross += result.totalGross;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
summary: {
|
|
158
|
+
totalEmployees: employees.length,
|
|
159
|
+
totalYearlyCost,
|
|
160
|
+
totalYearlyNet,
|
|
161
|
+
totalYearlyGross,
|
|
162
|
+
averageMonthlyCost: totalYearlyCost / periodCount
|
|
163
|
+
},
|
|
164
|
+
employees: employeeResults
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/types/index.ts
|
|
169
|
+
var DEFAULT_PARAMS_2025 = {
|
|
170
|
+
year: 2025,
|
|
171
|
+
minWage: 26005.5,
|
|
172
|
+
minWageNet: 22104.67,
|
|
173
|
+
ssiLowerLimit: 26005.5,
|
|
174
|
+
ssiUpperLimit: 195041.4,
|
|
175
|
+
stampTaxRatio: 759e-5,
|
|
176
|
+
incomeTaxBrackets: [
|
|
177
|
+
{ limit: 158e3, rate: 0.15, description: "158,000 TL'ye kadar %15" },
|
|
178
|
+
{ limit: 33e4, rate: 0.2, description: "158,000-330,000 TL aras\u0131 %20" },
|
|
179
|
+
{ limit: 12e5, rate: 0.27, description: "330,000-1,200,000 TL aras\u0131 %27" },
|
|
180
|
+
{ limit: 43e5, rate: 0.35, description: "1,200,000-4,300,000 TL aras\u0131 %35" },
|
|
181
|
+
{ limit: Number.MAX_SAFE_INTEGER, rate: 0.4, description: "4,300,000 TL'den fazlas\u0131 %40" }
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// src/tools/params.ts
|
|
186
|
+
function getDefaultParams(input) {
|
|
187
|
+
if (input.year === 2025) {
|
|
188
|
+
return DEFAULT_PARAMS_2025;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
...DEFAULT_PARAMS_2025,
|
|
192
|
+
year: input.year
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function applyScenario(defaults, scenario) {
|
|
196
|
+
const result = {};
|
|
197
|
+
if (scenario.minWage !== void 0) {
|
|
198
|
+
result.minWage = scenario.minWage;
|
|
199
|
+
}
|
|
200
|
+
if (scenario.minWageNet !== void 0) {
|
|
201
|
+
result.minWageNet = scenario.minWageNet;
|
|
202
|
+
}
|
|
203
|
+
if (scenario.ssiLimitIncreasePercent !== void 0) {
|
|
204
|
+
const multiplier = 1 + scenario.ssiLimitIncreasePercent / 100;
|
|
205
|
+
result.ssiLowerLimit = defaults.ssiLowerLimit * multiplier;
|
|
206
|
+
result.ssiUpperLimit = defaults.ssiUpperLimit * multiplier;
|
|
207
|
+
}
|
|
208
|
+
if (scenario.customTaxBrackets !== void 0) {
|
|
209
|
+
result.incomeTaxLimits = scenario.customTaxBrackets;
|
|
210
|
+
} else if (scenario.taxLimitIncreasePercent !== void 0) {
|
|
211
|
+
const multiplier = 1 + scenario.taxLimitIncreasePercent / 100;
|
|
212
|
+
result.incomeTaxLimits = defaults.incomeTaxBrackets.map((bracket) => ({
|
|
213
|
+
limit: bracket.limit === Number.MAX_SAFE_INTEGER ? bracket.limit : Math.round(bracket.limit * multiplier),
|
|
214
|
+
rate: bracket.rate
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
function applyRaise(wage, raisePercent) {
|
|
220
|
+
if (raisePercent === void 0 || raisePercent === 0) {
|
|
221
|
+
return wage;
|
|
222
|
+
}
|
|
223
|
+
return wage * (1 + raisePercent / 100);
|
|
224
|
+
}
|
|
225
|
+
async function simulateBudget(client, input) {
|
|
226
|
+
const { employees, year, periodCount, scenario } = input;
|
|
227
|
+
const defaults = getDefaultParams({ year });
|
|
228
|
+
const customParams = applyScenario(defaults, scenario);
|
|
229
|
+
const adjustedEmployees = employees.map((emp) => ({
|
|
230
|
+
name: emp.name,
|
|
231
|
+
wage: applyRaise(emp.wage, scenario.salaryRaisePercent),
|
|
232
|
+
calculationType: emp.calculationType,
|
|
233
|
+
originalWage: emp.wage
|
|
234
|
+
}));
|
|
235
|
+
const result = await calculateBulkPayroll(client, {
|
|
236
|
+
employees: adjustedEmployees.map((emp) => ({
|
|
237
|
+
name: emp.name,
|
|
238
|
+
wage: emp.wage,
|
|
239
|
+
calculationType: emp.calculationType
|
|
240
|
+
})),
|
|
241
|
+
year,
|
|
242
|
+
month: 1,
|
|
243
|
+
periodCount,
|
|
244
|
+
customParams
|
|
245
|
+
});
|
|
246
|
+
const employeeResults = adjustedEmployees.map((emp, index) => {
|
|
247
|
+
const empResult = result.employees[index];
|
|
248
|
+
return {
|
|
249
|
+
name: emp.name,
|
|
250
|
+
originalWage: emp.originalWage,
|
|
251
|
+
adjustedWage: emp.wage,
|
|
252
|
+
yearlyCost: empResult.totalCost,
|
|
253
|
+
yearlyNet: empResult.totalNet,
|
|
254
|
+
yearlyGross: empResult.totalGross
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
const effectiveTaxBrackets = customParams.incomeTaxLimits || defaults.incomeTaxBrackets.map((b) => ({
|
|
258
|
+
limit: b.limit,
|
|
259
|
+
rate: b.rate
|
|
260
|
+
}));
|
|
261
|
+
return {
|
|
262
|
+
scenarioApplied: {
|
|
263
|
+
salaryRaisePercent: scenario.salaryRaisePercent || 0,
|
|
264
|
+
effectiveMinWage: customParams.minWage || defaults.minWage,
|
|
265
|
+
effectiveMinWageNet: customParams.minWageNet || defaults.minWageNet,
|
|
266
|
+
effectiveTaxBrackets
|
|
267
|
+
},
|
|
268
|
+
summary: {
|
|
269
|
+
totalYearlyCost: result.summary.totalYearlyCost,
|
|
270
|
+
totalYearlyNet: result.summary.totalYearlyNet,
|
|
271
|
+
totalYearlyGross: result.summary.totalYearlyGross,
|
|
272
|
+
costPerEmployee: result.summary.totalYearlyCost / employees.length
|
|
273
|
+
},
|
|
274
|
+
employees: employeeResults
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
async function compareScenarios(client, input) {
|
|
278
|
+
const { employees, year, periodCount, scenarios } = input;
|
|
279
|
+
if (scenarios.length === 0) {
|
|
280
|
+
throw new Error("At least one scenario is required");
|
|
281
|
+
}
|
|
282
|
+
const results = [];
|
|
283
|
+
for (const scenario of scenarios) {
|
|
284
|
+
const result = await simulateBudget(client, {
|
|
285
|
+
employees,
|
|
286
|
+
year,
|
|
287
|
+
periodCount,
|
|
288
|
+
scenario
|
|
289
|
+
});
|
|
290
|
+
results.push({
|
|
291
|
+
name: scenario.name || `Scenario ${results.length + 1}`,
|
|
292
|
+
totalCost: result.summary.totalYearlyCost
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
const baselineCost = results[0].totalCost;
|
|
296
|
+
const comparison = results.map((r) => ({
|
|
297
|
+
scenarioName: r.name,
|
|
298
|
+
totalCost: r.totalCost,
|
|
299
|
+
costDifference: r.totalCost - baselineCost,
|
|
300
|
+
percentChange: baselineCost > 0 ? (r.totalCost - baselineCost) / baselineCost * 100 : 0
|
|
301
|
+
}));
|
|
302
|
+
const sortedByCoset = [...results].sort((a, b) => a.totalCost - b.totalCost);
|
|
303
|
+
const cheapestScenario = sortedByCoset[0].name;
|
|
304
|
+
const mostExpensiveScenario = sortedByCoset[sortedByCoset.length - 1].name;
|
|
305
|
+
return {
|
|
306
|
+
baselineCost,
|
|
307
|
+
comparison,
|
|
308
|
+
cheapestScenario,
|
|
309
|
+
mostExpensiveScenario
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/resources/index.ts
|
|
314
|
+
function registerResources(server) {
|
|
315
|
+
server.resource(
|
|
316
|
+
"2025 Turkish Payroll Defaults",
|
|
317
|
+
"payrolla://defaults/2025",
|
|
318
|
+
{
|
|
319
|
+
description: "Default payroll parameters for Turkey in 2025 including minimum wage, SSI limits, and tax brackets",
|
|
320
|
+
mimeType: "application/json"
|
|
321
|
+
},
|
|
322
|
+
async () => {
|
|
323
|
+
return {
|
|
324
|
+
contents: [
|
|
325
|
+
{
|
|
326
|
+
uri: "payrolla://defaults/2025",
|
|
327
|
+
mimeType: "application/json",
|
|
328
|
+
text: JSON.stringify(DEFAULT_PARAMS_2025, null, 2)
|
|
329
|
+
}
|
|
330
|
+
]
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/prompts/index.ts
|
|
337
|
+
import { z } from "zod";
|
|
338
|
+
function getBudgetSimulationPrompt(employeeCount, scenarioType) {
|
|
339
|
+
const scenarioText = scenarioType || "any type of";
|
|
340
|
+
return `You are helping calculate a payroll budget simulation for Turkish employees.
|
|
341
|
+
|
|
342
|
+
The user has ${employeeCount} employees. They want to simulate a ${scenarioText} scenario.
|
|
343
|
+
|
|
344
|
+
**Your task:**
|
|
345
|
+
|
|
346
|
+
1. First, ask the user for each employee's details:
|
|
347
|
+
- Name
|
|
348
|
+
- Current salary amount
|
|
349
|
+
- Whether it's Net or Gross salary
|
|
350
|
+
|
|
351
|
+
2. Ask about the scenario parameters they want to simulate:
|
|
352
|
+
- Salary raise percentage (e.g., 10% for a 10% raise)
|
|
353
|
+
- New minimum wage if different from current (current 2025: 26,005.50 TL gross)
|
|
354
|
+
- Tax bracket changes if any
|
|
355
|
+
|
|
356
|
+
3. Use the **simulate_budget** tool with the collected data to calculate results.
|
|
357
|
+
|
|
358
|
+
4. Present a clear summary showing:
|
|
359
|
+
- Current state vs. scenario comparison
|
|
360
|
+
- Total yearly cost difference
|
|
361
|
+
- Per-employee breakdown
|
|
362
|
+
|
|
363
|
+
**Important:** All monetary values should be in Turkish Lira (TL). Use the 2025 default parameters as baseline unless the user specifies otherwise.`;
|
|
364
|
+
}
|
|
365
|
+
function getSalaryRaiseAnalysisPrompt(raisePercentages) {
|
|
366
|
+
return `You are analyzing the cost impact of different salary raise scenarios for Turkish employees.
|
|
367
|
+
|
|
368
|
+
The user wants to compare these raise percentages: ${raisePercentages}
|
|
369
|
+
|
|
370
|
+
**Your task:**
|
|
371
|
+
|
|
372
|
+
1. Ask the user for their employee list:
|
|
373
|
+
- Each employee's name
|
|
374
|
+
- Current salary (Net or Gross)
|
|
375
|
+
- Salary type (Net or Gross)
|
|
376
|
+
|
|
377
|
+
2. Use the **compare_scenarios** tool to calculate all raise scenarios side by side.
|
|
378
|
+
- Create one scenario for each raise percentage
|
|
379
|
+
- Use 12 period count for yearly calculation
|
|
380
|
+
|
|
381
|
+
3. Present results in a clear comparison table:
|
|
382
|
+
- Scenario name
|
|
383
|
+
- Total yearly cost
|
|
384
|
+
- Cost difference from baseline (0% raise)
|
|
385
|
+
- Percentage change
|
|
386
|
+
|
|
387
|
+
4. Provide a recommendation on the most cost-effective option.
|
|
388
|
+
|
|
389
|
+
**Note:** The comparison uses Turkish payroll rules including progressive income tax, SSI contributions, and stamp tax.`;
|
|
390
|
+
}
|
|
391
|
+
function getYearPlanningPrompt(planningYear) {
|
|
392
|
+
return `You are helping plan the ${planningYear} payroll budget for a Turkish company.
|
|
393
|
+
|
|
394
|
+
**Your task:**
|
|
395
|
+
|
|
396
|
+
1. First, use **get_default_params** to show the current ${planningYear} parameters:
|
|
397
|
+
- Minimum wage (gross and net)
|
|
398
|
+
- SSI contribution limits
|
|
399
|
+
- Income tax brackets
|
|
400
|
+
- Stamp tax ratio
|
|
401
|
+
|
|
402
|
+
2. Ask the user about their workforce:
|
|
403
|
+
- Number of employees
|
|
404
|
+
- Current salaries for each employee
|
|
405
|
+
|
|
406
|
+
3. Ask about expected changes for ${planningYear}:
|
|
407
|
+
- Expected minimum wage increase (if any)
|
|
408
|
+
- Expected tax bracket adjustments
|
|
409
|
+
- Planned salary raises for employees
|
|
410
|
+
|
|
411
|
+
4. Use **simulate_budget** to calculate:
|
|
412
|
+
- Current cost (no changes)
|
|
413
|
+
- Projected cost with expected changes
|
|
414
|
+
|
|
415
|
+
5. Provide a comprehensive summary:
|
|
416
|
+
- Monthly and yearly cost projections
|
|
417
|
+
- Budget increase/decrease compared to current
|
|
418
|
+
- Per-employee cost breakdown
|
|
419
|
+
- Recommendations for budget planning
|
|
420
|
+
|
|
421
|
+
**Currency:** All values in Turkish Lira (TL)`;
|
|
422
|
+
}
|
|
423
|
+
function registerPrompts(server) {
|
|
424
|
+
server.prompt(
|
|
425
|
+
"budget_simulation",
|
|
426
|
+
"Simulate yearly payroll budget with custom scenarios like salary raises, minimum wage changes, or tax adjustments",
|
|
427
|
+
{
|
|
428
|
+
employee_count: z.string().describe("Number of employees to simulate"),
|
|
429
|
+
scenario_type: z.string().optional().describe("Type of scenario: raise, min_wage_change, tax_change, or combined")
|
|
430
|
+
},
|
|
431
|
+
async (args) => {
|
|
432
|
+
return {
|
|
433
|
+
messages: [
|
|
434
|
+
{
|
|
435
|
+
role: "user",
|
|
436
|
+
content: {
|
|
437
|
+
type: "text",
|
|
438
|
+
text: getBudgetSimulationPrompt(
|
|
439
|
+
args.employee_count || "(unspecified)",
|
|
440
|
+
args.scenario_type
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
]
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
server.prompt(
|
|
449
|
+
"salary_raise_analysis",
|
|
450
|
+
"Analyze the cost impact of giving different salary raise percentages",
|
|
451
|
+
{
|
|
452
|
+
raise_percentages: z.string().describe('Comma-separated raise percentages to compare (e.g., "5,10,15")')
|
|
453
|
+
},
|
|
454
|
+
async (args) => {
|
|
455
|
+
return {
|
|
456
|
+
messages: [
|
|
457
|
+
{
|
|
458
|
+
role: "user",
|
|
459
|
+
content: {
|
|
460
|
+
type: "text",
|
|
461
|
+
text: getSalaryRaiseAnalysisPrompt(args.raise_percentages || "5,10,15")
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
]
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
);
|
|
468
|
+
server.prompt(
|
|
469
|
+
"year_planning",
|
|
470
|
+
"Plan yearly payroll considering potential minimum wage and tax changes",
|
|
471
|
+
{
|
|
472
|
+
planning_year: z.string().describe("Year to plan for (e.g., 2025)")
|
|
473
|
+
},
|
|
474
|
+
async (args) => {
|
|
475
|
+
return {
|
|
476
|
+
messages: [
|
|
477
|
+
{
|
|
478
|
+
role: "user",
|
|
479
|
+
content: {
|
|
480
|
+
type: "text",
|
|
481
|
+
text: getYearPlanningPrompt(args.planning_year || "2025")
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
]
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/server.ts
|
|
491
|
+
var ExtraPaymentSchema = z2.object({
|
|
492
|
+
name: z2.string().describe("Name of the extra payment"),
|
|
493
|
+
amount: z2.number().describe("Payment amount"),
|
|
494
|
+
type: z2.enum(["Net", "Gross"]).describe("Payment type")
|
|
495
|
+
});
|
|
496
|
+
var CustomParamsSchema = z2.object({
|
|
497
|
+
minWage: z2.number().optional().describe("Custom minimum wage (gross)"),
|
|
498
|
+
minWageNet: z2.number().optional().describe("Custom minimum wage (net)"),
|
|
499
|
+
ssiLowerLimit: z2.number().optional().describe("Custom SSI lower limit"),
|
|
500
|
+
ssiUpperLimit: z2.number().optional().describe("Custom SSI upper limit"),
|
|
501
|
+
stampTaxRatio: z2.number().optional().describe("Custom stamp tax ratio"),
|
|
502
|
+
incomeTaxLimits: z2.array(z2.object({
|
|
503
|
+
limit: z2.number().describe("Upper limit for this bracket"),
|
|
504
|
+
rate: z2.number().describe("Tax rate (e.g., 0.15 for 15%)")
|
|
505
|
+
})).optional().describe("Custom income tax brackets")
|
|
506
|
+
});
|
|
507
|
+
var EmployeeInputSchema = z2.object({
|
|
508
|
+
name: z2.string().describe("Employee name"),
|
|
509
|
+
wage: z2.number().describe("Wage amount"),
|
|
510
|
+
calculationType: z2.enum(["Gross", "Net"]).describe("Whether wage is gross or net"),
|
|
511
|
+
ssiType: z2.enum(["S4A", "S4B", "S4C"]).optional().describe("SSI type (default: S4A)"),
|
|
512
|
+
extraPayments: z2.array(ExtraPaymentSchema).optional().describe("Extra payments like bonuses")
|
|
513
|
+
});
|
|
514
|
+
var ScenarioConfigSchema = z2.object({
|
|
515
|
+
name: z2.string().optional().describe("Scenario name for comparison"),
|
|
516
|
+
salaryRaisePercent: z2.number().optional().describe("Salary raise percentage (e.g., 10 for 10%)"),
|
|
517
|
+
minWage: z2.number().optional().describe("Custom minimum wage"),
|
|
518
|
+
minWageNet: z2.number().optional().describe("Custom net minimum wage"),
|
|
519
|
+
taxLimitIncreasePercent: z2.number().optional().describe("Increase tax bracket limits by percentage"),
|
|
520
|
+
ssiLimitIncreasePercent: z2.number().optional().describe("Increase SSI limits by percentage"),
|
|
521
|
+
customTaxBrackets: z2.array(z2.object({
|
|
522
|
+
limit: z2.number(),
|
|
523
|
+
rate: z2.number()
|
|
524
|
+
})).optional().describe("Custom tax brackets")
|
|
525
|
+
});
|
|
526
|
+
function createServer() {
|
|
527
|
+
const apiKey = process.env.PAYROLLA_API_KEY;
|
|
528
|
+
if (!apiKey) {
|
|
529
|
+
console.error("Error: PAYROLLA_API_KEY environment variable is required");
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
const payrollaClient = new PayrollaClient2({
|
|
533
|
+
apiKey,
|
|
534
|
+
timeout: 3e4
|
|
535
|
+
});
|
|
536
|
+
const server = new McpServer({
|
|
537
|
+
name: "payrolla-mcp",
|
|
538
|
+
version: "1.0.0"
|
|
539
|
+
});
|
|
540
|
+
registerTools(server, payrollaClient);
|
|
541
|
+
registerResources(server);
|
|
542
|
+
registerPrompts(server);
|
|
543
|
+
return server;
|
|
544
|
+
}
|
|
545
|
+
function registerTools(server, client) {
|
|
546
|
+
server.tool(
|
|
547
|
+
"calculate_payroll",
|
|
548
|
+
"Calculate payroll for a single employee including taxes, SSI, and employer cost",
|
|
549
|
+
{
|
|
550
|
+
name: z2.string().describe("Employee name"),
|
|
551
|
+
wage: z2.number().describe("Wage amount"),
|
|
552
|
+
calculationType: z2.enum(["Gross", "Net"]).describe("Whether the wage is gross or net"),
|
|
553
|
+
ssiType: z2.enum(["S4A", "S4B", "S4C"]).optional().describe("SSI type (default: S4A for general employees)"),
|
|
554
|
+
year: z2.number().describe("Calculation year (e.g., 2025)"),
|
|
555
|
+
month: z2.number().min(1).max(12).describe("Starting month (1-12)"),
|
|
556
|
+
periodCount: z2.number().min(1).max(12).optional().describe("Number of months to calculate (default: 1)"),
|
|
557
|
+
extraPayments: z2.array(ExtraPaymentSchema).optional().describe("Extra payments like bonuses"),
|
|
558
|
+
customParams: CustomParamsSchema.optional().describe("Custom global parameters to override defaults")
|
|
559
|
+
},
|
|
560
|
+
async (params) => {
|
|
561
|
+
try {
|
|
562
|
+
const result = await calculatePayroll(client, params);
|
|
563
|
+
return {
|
|
564
|
+
content: [
|
|
565
|
+
{
|
|
566
|
+
type: "text",
|
|
567
|
+
text: JSON.stringify(result, null, 2)
|
|
568
|
+
}
|
|
569
|
+
]
|
|
570
|
+
};
|
|
571
|
+
} catch (error) {
|
|
572
|
+
return {
|
|
573
|
+
content: [
|
|
574
|
+
{
|
|
575
|
+
type: "text",
|
|
576
|
+
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
577
|
+
}
|
|
578
|
+
],
|
|
579
|
+
isError: true
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
);
|
|
584
|
+
server.tool(
|
|
585
|
+
"calculate_bulk_payroll",
|
|
586
|
+
"Calculate payroll for multiple employees with shared parameters",
|
|
587
|
+
{
|
|
588
|
+
employees: z2.array(EmployeeInputSchema).describe("Array of employees to calculate"),
|
|
589
|
+
year: z2.number().describe("Calculation year (e.g., 2025)"),
|
|
590
|
+
month: z2.number().min(1).max(12).describe("Starting month (1-12)"),
|
|
591
|
+
periodCount: z2.number().min(1).max(12).optional().describe("Number of months (default: 1, use 12 for yearly)"),
|
|
592
|
+
customParams: CustomParamsSchema.optional().describe("Custom global parameters shared by all employees")
|
|
593
|
+
},
|
|
594
|
+
async (params) => {
|
|
595
|
+
try {
|
|
596
|
+
const result = await calculateBulkPayroll(client, params);
|
|
597
|
+
return {
|
|
598
|
+
content: [
|
|
599
|
+
{
|
|
600
|
+
type: "text",
|
|
601
|
+
text: JSON.stringify(result, null, 2)
|
|
602
|
+
}
|
|
603
|
+
]
|
|
604
|
+
};
|
|
605
|
+
} catch (error) {
|
|
606
|
+
return {
|
|
607
|
+
content: [
|
|
608
|
+
{
|
|
609
|
+
type: "text",
|
|
610
|
+
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
611
|
+
}
|
|
612
|
+
],
|
|
613
|
+
isError: true
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
);
|
|
618
|
+
server.tool(
|
|
619
|
+
"simulate_budget",
|
|
620
|
+
"Simulate budget with what-if scenarios like salary raises or parameter changes",
|
|
621
|
+
{
|
|
622
|
+
employees: z2.array(z2.object({
|
|
623
|
+
name: z2.string().describe("Employee name"),
|
|
624
|
+
wage: z2.number().describe("Current wage amount"),
|
|
625
|
+
calculationType: z2.enum(["Gross", "Net"]).describe("Wage type")
|
|
626
|
+
})).describe("Array of employees"),
|
|
627
|
+
year: z2.number().describe("Calculation year"),
|
|
628
|
+
periodCount: z2.number().min(1).max(12).describe("Number of months (use 12 for yearly)"),
|
|
629
|
+
scenario: ScenarioConfigSchema.describe("Scenario configuration with changes to apply")
|
|
630
|
+
},
|
|
631
|
+
async (params) => {
|
|
632
|
+
try {
|
|
633
|
+
const result = await simulateBudget(client, params);
|
|
634
|
+
return {
|
|
635
|
+
content: [
|
|
636
|
+
{
|
|
637
|
+
type: "text",
|
|
638
|
+
text: JSON.stringify(result, null, 2)
|
|
639
|
+
}
|
|
640
|
+
]
|
|
641
|
+
};
|
|
642
|
+
} catch (error) {
|
|
643
|
+
return {
|
|
644
|
+
content: [
|
|
645
|
+
{
|
|
646
|
+
type: "text",
|
|
647
|
+
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
648
|
+
}
|
|
649
|
+
],
|
|
650
|
+
isError: true
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
);
|
|
655
|
+
server.tool(
|
|
656
|
+
"compare_scenarios",
|
|
657
|
+
"Compare multiple budget scenarios side by side",
|
|
658
|
+
{
|
|
659
|
+
employees: z2.array(z2.object({
|
|
660
|
+
name: z2.string().describe("Employee name"),
|
|
661
|
+
wage: z2.number().describe("Current wage"),
|
|
662
|
+
calculationType: z2.enum(["Gross", "Net"]).describe("Wage type")
|
|
663
|
+
})).describe("Array of employees"),
|
|
664
|
+
year: z2.number().describe("Calculation year"),
|
|
665
|
+
periodCount: z2.number().min(1).max(12).describe("Number of months"),
|
|
666
|
+
scenarios: z2.array(ScenarioConfigSchema).min(1).describe("Array of scenarios to compare")
|
|
667
|
+
},
|
|
668
|
+
async (params) => {
|
|
669
|
+
try {
|
|
670
|
+
const result = await compareScenarios(client, params);
|
|
671
|
+
return {
|
|
672
|
+
content: [
|
|
673
|
+
{
|
|
674
|
+
type: "text",
|
|
675
|
+
text: JSON.stringify(result, null, 2)
|
|
676
|
+
}
|
|
677
|
+
]
|
|
678
|
+
};
|
|
679
|
+
} catch (error) {
|
|
680
|
+
return {
|
|
681
|
+
content: [
|
|
682
|
+
{
|
|
683
|
+
type: "text",
|
|
684
|
+
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
685
|
+
}
|
|
686
|
+
],
|
|
687
|
+
isError: true
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
);
|
|
692
|
+
server.tool(
|
|
693
|
+
"get_default_params",
|
|
694
|
+
"Get default Turkish payroll parameters for a given year",
|
|
695
|
+
{
|
|
696
|
+
year: z2.number().describe("Year to get parameters for (e.g., 2025)")
|
|
697
|
+
},
|
|
698
|
+
async (params) => {
|
|
699
|
+
try {
|
|
700
|
+
const result = getDefaultParams(params);
|
|
701
|
+
return {
|
|
702
|
+
content: [
|
|
703
|
+
{
|
|
704
|
+
type: "text",
|
|
705
|
+
text: JSON.stringify(result, null, 2)
|
|
706
|
+
}
|
|
707
|
+
]
|
|
708
|
+
};
|
|
709
|
+
} catch (error) {
|
|
710
|
+
return {
|
|
711
|
+
content: [
|
|
712
|
+
{
|
|
713
|
+
type: "text",
|
|
714
|
+
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
715
|
+
}
|
|
716
|
+
],
|
|
717
|
+
isError: true
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// src/index.ts
|
|
725
|
+
async function main() {
|
|
726
|
+
if (!process.env.PAYROLLA_API_KEY) {
|
|
727
|
+
console.error("Error: PAYROLLA_API_KEY environment variable is required");
|
|
728
|
+
console.error("");
|
|
729
|
+
console.error("Set it in your MCP client configuration or run:");
|
|
730
|
+
console.error(" export PAYROLLA_API_KEY=pk_live_xxxxx");
|
|
731
|
+
process.exit(1);
|
|
732
|
+
}
|
|
733
|
+
const debug = process.env.PAYROLLA_DEBUG === "true";
|
|
734
|
+
if (debug) {
|
|
735
|
+
console.error("[payrolla-mcp] Starting in debug mode...");
|
|
736
|
+
}
|
|
737
|
+
try {
|
|
738
|
+
const server = createServer();
|
|
739
|
+
const transport = new StdioServerTransport();
|
|
740
|
+
await server.connect(transport);
|
|
741
|
+
if (debug) {
|
|
742
|
+
console.error("[payrolla-mcp] Server connected via stdio");
|
|
743
|
+
}
|
|
744
|
+
process.on("SIGINT", async () => {
|
|
745
|
+
if (debug) {
|
|
746
|
+
console.error("[payrolla-mcp] Shutting down...");
|
|
747
|
+
}
|
|
748
|
+
await server.close();
|
|
749
|
+
process.exit(0);
|
|
750
|
+
});
|
|
751
|
+
process.on("SIGTERM", async () => {
|
|
752
|
+
if (debug) {
|
|
753
|
+
console.error("[payrolla-mcp] Shutting down...");
|
|
754
|
+
}
|
|
755
|
+
await server.close();
|
|
756
|
+
process.exit(0);
|
|
757
|
+
});
|
|
758
|
+
} catch (error) {
|
|
759
|
+
console.error("Fatal error:", error);
|
|
760
|
+
process.exit(1);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
main().catch((error) => {
|
|
764
|
+
console.error("Unhandled error:", error);
|
|
765
|
+
process.exit(1);
|
|
766
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "payrolla-mcp",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "MCP server for Payrolla payroll budget simulations - enables LLMs to calculate Turkish payroll and simulate budget scenarios",
|
|
5
|
+
"author": "Payrolla",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/runpayrolla/payrolla-mcp.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/runpayrolla/payrolla-mcp#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/runpayrolla/payrolla-mcp/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"bin": {
|
|
19
|
+
"payrolla-mcp": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
26
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"mcp",
|
|
32
|
+
"model-context-protocol",
|
|
33
|
+
"payrolla",
|
|
34
|
+
"payroll",
|
|
35
|
+
"budget",
|
|
36
|
+
"simulation",
|
|
37
|
+
"turkish",
|
|
38
|
+
"turkey",
|
|
39
|
+
"salary",
|
|
40
|
+
"tax",
|
|
41
|
+
"ssi",
|
|
42
|
+
"llm",
|
|
43
|
+
"ai"
|
|
44
|
+
],
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
50
|
+
"payrolla": "^0.2.1",
|
|
51
|
+
"zod": "^3.25.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^20.0.0",
|
|
55
|
+
"tsup": "^8.0.0",
|
|
56
|
+
"typescript": "^5.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|