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 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?
@@ -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
+ }