@palpluss/mcp 0.0.0-stage → 0.1.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.
@@ -0,0 +1,279 @@
1
+ import { z } from 'zod';
2
+ import { assertAmount, guarded, idempotencyKey, kes, ok, requireApproval, requirePhone, ToolRefusal, } from './common.js';
3
+ import { maskPhone } from '../safety/redact.js';
4
+ const WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
5
+ const FINAL = new Set(['SUCCESS', 'FAILED', 'CANCELLED', 'REVERSED']);
6
+ const amountSchema = z.number().int().positive().describe('Whole Kenyan shillings.');
7
+ const phoneSchema = z.string().min(9).max(16).describe('Kenyan mobile number: 07XXXXXXXX, 01XXXXXXXX or 2547XXXXXXXX.');
8
+ const callbackSchema = z.string().url().max(500).optional().describe('HTTPS URL PalPluss POSTs the final result to.');
9
+ /** Wallet top-ups: STK push to the payer's own phone, credited once M-Pesa confirms. */
10
+ export function registerWalletTools(ctx) {
11
+ const { server, client, config } = ctx;
12
+ for (const wallet of ['service', 'b2c']) {
13
+ const name = wallet === 'service' ? 'palpluss_topup_service_wallet' : 'palpluss_topup_b2c_wallet';
14
+ const label = wallet === 'service' ? 'service wallet (fees)' : 'B2C wallet (payout float)';
15
+ server.registerTool(name, {
16
+ title: wallet === 'service' ? 'Top up service wallet' : 'Top up B2C wallet',
17
+ description: wallet === 'service'
18
+ ? 'Top up the service wallet, which pays PalPluss fees. Sends an M-Pesa PIN prompt to the given phone; the wallet is credited once it is paid. Needs the person\'s approval first.'
19
+ : 'Top up the B2C wallet, the float payouts are sent from. Sends an M-Pesa PIN prompt to the given phone; the wallet is credited once it is paid. Needs the person\'s approval first.',
20
+ inputSchema: {
21
+ amount: amountSchema,
22
+ phone: phoneSchema.describe('The phone that will pay (gets the PIN prompt).'),
23
+ reference: z.string().max(12).optional().describe('Your reference, ≤12 chars. Use a new one for each separate top-up.'),
24
+ callbackUrl: callbackSchema,
25
+ },
26
+ annotations: { title: `Top up ${label}`, ...WRITE },
27
+ }, guarded(ctx, name, async ({ amount, phone, reference, callbackUrl }) => {
28
+ assertAmount(amount, { min: 1, max: config.limits.maxTopup, label: 'Top-up' });
29
+ const msisdn = requirePhone(phone);
30
+ const approval = await requireApproval(ctx, {
31
+ action: `${wallet}_wallet_topup`,
32
+ title: `Top up the ${label} with ${kes(amount)}`,
33
+ details: { Amount: kes(amount), 'PIN prompt goes to': maskPhone(msisdn) ?? '', Wallet: label },
34
+ consequence: `An M-Pesa prompt for ${kes(amount)} will be sent to ${maskPhone(msisdn)}. The wallet is credited only after that phone pays.`,
35
+ });
36
+ const body = {
37
+ amount,
38
+ phone: msisdn,
39
+ accountReference: reference ?? (wallet === 'service' ? 'MCP-TOPUP' : 'MCP-B2CTOP'),
40
+ transactionDesc: wallet === 'service' ? 'Service topup' : 'B2C topup',
41
+ ...(callbackUrl ? { callbackUrl } : {}),
42
+ };
43
+ const key = idempotencyKey(config.apiKey, [`topup-${wallet}`, amount, msisdn], reference);
44
+ const res = wallet === 'service' ? await client.topupServiceWallet(body, key) : await client.topupB2cWallet(body, key);
45
+ return ok(res, {
46
+ note: `Approved (${approval}). The PIN prompt was sent.`,
47
+ next: 'Use palpluss_get_transaction with the transactionId to see when it is paid, then palpluss_get_wallet_balances.',
48
+ });
49
+ }));
50
+ }
51
+ }
52
+ /** Collections: STK push to a customer, and waiting for the result. */
53
+ export function registerCollectTools(ctx) {
54
+ const { server, client, config } = ctx;
55
+ server.registerTool('palpluss_send_stk_push', {
56
+ title: 'Send an STK push',
57
+ description: 'Ask a customer to pay by sending an M-Pesa PIN prompt to their phone. Money goes to the default payment channel unless channelId is given. Needs the person\'s approval first. Returns a PENDING transaction — follow it with palpluss_wait_for_payment.',
58
+ inputSchema: {
59
+ amount: amountSchema,
60
+ phone: phoneSchema.describe('The customer\'s phone (gets the PIN prompt).'),
61
+ accountReference: z.string().max(12).optional().describe('Your order/invoice reference, ≤12 chars.'),
62
+ description: z.string().max(13).optional().describe('Shown on the prompt, ≤13 chars.'),
63
+ channelId: z.string().uuid().optional().describe('Payment channel to collect into (default channel if omitted).'),
64
+ callbackUrl: callbackSchema,
65
+ },
66
+ annotations: { title: 'Send an STK push', ...WRITE },
67
+ }, guarded(ctx, 'palpluss_send_stk_push', async ({ amount, phone, accountReference, description, channelId, callbackUrl }) => {
68
+ assertAmount(amount, { min: 1, max: config.limits.maxStk, label: 'STK push' });
69
+ const msisdn = requirePhone(phone);
70
+ let channelLabel = 'Default channel';
71
+ if (channelId) {
72
+ const ch = (await client.getChannel(channelId));
73
+ channelLabel = `${ch.name ?? 'Channel'} (${ch.shortcode ?? '?'})`;
74
+ }
75
+ const approval = await requireApproval(ctx, {
76
+ action: 'stk_push',
77
+ title: `Request ${kes(amount)} from ${maskPhone(msisdn)}`,
78
+ details: {
79
+ Amount: kes(amount),
80
+ 'Customer phone': maskPhone(msisdn) ?? '',
81
+ 'Collect into': channelLabel,
82
+ ...(accountReference ? { Reference: accountReference } : {}),
83
+ },
84
+ consequence: `The customer at ${maskPhone(msisdn)} gets an M-Pesa prompt to pay ${kes(amount)}.`,
85
+ });
86
+ const res = await client.stkPush({
87
+ amount,
88
+ phone: msisdn,
89
+ ...(accountReference ? { accountReference } : {}),
90
+ ...(description ? { transactionDesc: description } : {}),
91
+ ...(channelId ? { channelId } : {}),
92
+ ...(callbackUrl ? { callbackUrl } : {}),
93
+ });
94
+ return ok(res, {
95
+ note: `Approved (${approval}). The prompt was sent; the customer has about a minute to enter their PIN.`,
96
+ next: 'Call palpluss_wait_for_payment with the transactionId.',
97
+ });
98
+ }));
99
+ server.registerTool('palpluss_wait_for_payment', {
100
+ title: 'Wait for a payment result',
101
+ description: 'Wait (up to 90 seconds) for an STK push, top-up or payout to reach a final status — SUCCESS, FAILED, CANCELLED or REVERSED — then return it. Returns the current status if it is still pending when time runs out.',
102
+ inputSchema: {
103
+ transactionId: z.string().uuid(),
104
+ timeoutSeconds: z.number().int().min(5).max(90).optional().describe('How long to wait (default 60).'),
105
+ },
106
+ annotations: { title: 'Wait for a payment result', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
107
+ }, guarded(ctx, 'palpluss_wait_for_payment', async ({ transactionId, timeoutSeconds }) => {
108
+ const deadline = Date.now() + (timeoutSeconds ?? 60) * 1000;
109
+ let tx = (await client.getTransaction(transactionId));
110
+ while (!FINAL.has(String(tx.status)) && Date.now() < deadline) {
111
+ await new Promise((r) => setTimeout(r, 3000));
112
+ tx = (await client.getTransaction(transactionId));
113
+ }
114
+ const final = FINAL.has(String(tx.status));
115
+ return ok(tx, {
116
+ note: final ? `Final status: ${tx.status}.` : `Still ${tx.status ?? 'pending'} — check again shortly.`,
117
+ });
118
+ }));
119
+ }
120
+ /** B2C payouts — the strictest tool: limits, allowlist, approval, idempotency. */
121
+ export function registerPayoutTools(ctx) {
122
+ const { server, client, config, limiter } = ctx;
123
+ server.registerTool('palpluss_send_payout', {
124
+ title: 'Send a B2C payout',
125
+ description: 'Send money from the B2C wallet to an M-Pesa number (refunds, commissions, salaries). Needs the person\'s approval and is capped by this server\'s payout limits — check palpluss_get_guardrails first. Give each separate payout its own reference; repeating the same request with the same reference never pays twice.',
126
+ inputSchema: {
127
+ amount: amountSchema.describe('Whole Kenyan shillings, minimum 10.'),
128
+ phone: phoneSchema.describe('The recipient\'s M-Pesa number.'),
129
+ reference: z.string().max(120).optional().describe('Unique reference for this payout (recommended).'),
130
+ description: z.string().max(255).optional().describe('What the payout is for.'),
131
+ channelId: z.string().uuid().optional(),
132
+ callbackUrl: callbackSchema,
133
+ },
134
+ annotations: { title: 'Send a B2C payout', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
135
+ }, guarded(ctx, 'palpluss_send_payout', async ({ amount, phone, reference, description, channelId, callbackUrl }) => {
136
+ assertAmount(amount, { min: 10, max: Math.max(10, config.limits.maxPayout), label: 'Payout' });
137
+ const msisdn = requirePhone(phone);
138
+ limiter.check(amount, msisdn); // fail fast before bothering the person
139
+ const left = limiter.remaining();
140
+ const approval = await requireApproval(ctx, {
141
+ action: 'b2c_payout',
142
+ title: `Send ${kes(amount)} to ${maskPhone(msisdn)}`,
143
+ details: {
144
+ Amount: kes(amount),
145
+ Recipient: maskPhone(msisdn) ?? '',
146
+ ...(reference ? { Reference: reference } : {}),
147
+ ...(description ? { For: description } : {}),
148
+ 'Left today after this': `${kes(Math.max(0, left.remainingTotal - amount))} · ${Math.max(0, left.remainingCount - 1)} payouts`,
149
+ },
150
+ consequence: `${kes(amount)} will leave your B2C wallet and be sent to ${maskPhone(msisdn)}. This can't be undone once M-Pesa pays it.`,
151
+ });
152
+ const release = limiter.reserve(amount, msisdn); // re-checks: limits may have moved while waiting
153
+ try {
154
+ const res = await client.b2cPayout({
155
+ amount,
156
+ phone: msisdn,
157
+ currency: 'KES',
158
+ ...(reference ? { reference } : {}),
159
+ ...(description ? { description } : {}),
160
+ ...(channelId ? { channelId } : {}),
161
+ ...(callbackUrl ? { callbackUrl } : {}),
162
+ }, idempotencyKey(config.apiKey, ['payout', amount, msisdn], reference));
163
+ const status = String(res.status ?? '');
164
+ if (status === 'FAILED')
165
+ release();
166
+ return ok(res, {
167
+ note: `Approved (${approval}). Payout submitted with status ${status || 'unknown'}.`,
168
+ next: 'Call palpluss_wait_for_payment with the transactionId to confirm it was paid.',
169
+ });
170
+ }
171
+ catch (err) {
172
+ release();
173
+ throw err;
174
+ }
175
+ }));
176
+ }
177
+ /** Payment channels — every change needs approval: channels decide where money lands. */
178
+ export function registerChannelTools(ctx) {
179
+ const { server, client } = ctx;
180
+ const CHANNEL_TYPES = ['PAYBILL', 'TILL', 'BUYGOODS', 'SHORTCODE'];
181
+ const describe = (c) => `${c.name ?? 'Unnamed'} — ${c.type ?? '?'} ${c.shortcode ?? '?'}${c.accountNumber ? ` / account ${c.accountNumber}` : ''}`;
182
+ server.registerTool('palpluss_create_payment_channel', {
183
+ title: 'Add a payment channel',
184
+ description: 'Add a Paybill or Till that customer payments can be collected into. For a bank account, use the bank\'s M-Pesa Paybill as shortcode and the bank account number as accountNumber. Needs the person\'s approval.',
185
+ inputSchema: {
186
+ name: z.string().min(1).max(80),
187
+ type: z.enum(CHANNEL_TYPES).describe('PAYBILL needs accountNumber; TILL/BUYGOODS/SHORTCODE must not have one.'),
188
+ shortcode: z.string().regex(/^\d{4,10}$/).describe('The Paybill or Till number.'),
189
+ accountNumber: z.string().min(1).max(30).optional(),
190
+ makeDefault: z.boolean().optional().describe('Make this the channel STK pushes use by default.'),
191
+ },
192
+ annotations: { title: 'Add a payment channel', ...WRITE },
193
+ }, guarded(ctx, 'palpluss_create_payment_channel', async ({ name, type, shortcode, accountNumber, makeDefault }) => {
194
+ if (type === 'PAYBILL' && !accountNumber)
195
+ throw new ToolRefusal('A PAYBILL channel needs an accountNumber.', 'ACCOUNT_NUMBER_REQUIRED');
196
+ if (type !== 'PAYBILL' && accountNumber)
197
+ throw new ToolRefusal(`A ${type} channel must not have an accountNumber.`, 'ACCOUNT_NUMBER_NOT_ALLOWED');
198
+ const approval = await requireApproval(ctx, {
199
+ action: 'channel_create',
200
+ title: `Add payment channel "${name}"`,
201
+ details: {
202
+ Channel: describe({ name, type, shortcode, accountNumber }),
203
+ 'Make default': makeDefault ? 'Yes — new STK payments will go here' : 'No',
204
+ },
205
+ consequence: makeDefault
206
+ ? `Customer payments will start going to ${type} ${shortcode}${accountNumber ? ` (account ${accountNumber})` : ''}. Check the numbers carefully.`
207
+ : `The channel is added but not used by default.`,
208
+ });
209
+ const res = await client.createChannel({ name, type, shortcode, ...(accountNumber ? { accountNumber } : {}), isDefault: !!makeDefault });
210
+ return ok(res, { note: `Approved (${approval}). Channel added.` });
211
+ }));
212
+ server.registerTool('palpluss_update_payment_channel', {
213
+ title: 'Edit a payment channel',
214
+ description: 'Rename a channel or change its number or account number. Needs the person\'s approval.',
215
+ inputSchema: {
216
+ channelId: z.string().uuid(),
217
+ name: z.string().min(1).max(80).optional(),
218
+ type: z.enum(CHANNEL_TYPES).optional(),
219
+ shortcode: z.string().regex(/^\d{4,10}$/).optional(),
220
+ accountNumber: z.string().min(1).max(30).nullable().optional(),
221
+ },
222
+ annotations: { title: 'Edit a payment channel', ...WRITE, idempotentHint: true },
223
+ }, guarded(ctx, 'palpluss_update_payment_channel', async ({ channelId, ...changes }) => {
224
+ const patch = Object.fromEntries(Object.entries(changes).filter(([, v]) => v !== undefined));
225
+ if (!Object.keys(patch).length)
226
+ throw new ToolRefusal('Nothing to change — pass at least one field.', 'NO_CHANGES');
227
+ const current = (await client.getChannel(channelId));
228
+ const next = { ...current, ...patch };
229
+ const approval = await requireApproval(ctx, {
230
+ action: 'channel_update',
231
+ title: `Change payment channel "${String(current.name ?? channelId)}"`,
232
+ details: { Now: describe(current), 'After the change': describe(next), Default: current.isDefault ? 'Yes' : 'No' },
233
+ consequence: current.isDefault
234
+ ? 'This is your default channel: new customer payments will go to the changed number.'
235
+ : 'Payments that use this channel will go to the changed number.',
236
+ });
237
+ const res = await client.updateChannel(channelId, patch);
238
+ return ok(res, { note: `Approved (${approval}). Channel updated.` });
239
+ }));
240
+ server.registerTool('palpluss_set_default_payment_channel', {
241
+ title: 'Set the default payment channel',
242
+ description: 'Make a channel the default — STK pushes without a channelId collect into it. Needs the person\'s approval.',
243
+ inputSchema: { channelId: z.string().uuid() },
244
+ annotations: { title: 'Set the default payment channel', ...WRITE, idempotentHint: true },
245
+ }, guarded(ctx, 'palpluss_set_default_payment_channel', async ({ channelId }) => {
246
+ const target = (await client.getChannel(channelId));
247
+ if (target.isDefault)
248
+ return ok(target, { note: 'Already the default channel. Nothing changed.' });
249
+ const list = (await client.listChannels());
250
+ const currentDefault = list.items?.find((c) => c.isDefault);
251
+ const approval = await requireApproval(ctx, {
252
+ action: 'channel_set_default',
253
+ title: `Make "${String(target.name ?? channelId)}" the default channel`,
254
+ details: {
255
+ 'New default': describe(target),
256
+ 'Current default': currentDefault ? describe(currentDefault) : 'None',
257
+ },
258
+ consequence: 'All new STK payments without a specific channel will go to the new default.',
259
+ });
260
+ const res = await client.updateChannel(channelId, { isDefault: true });
261
+ return ok(res, { note: `Approved (${approval}). Default channel changed.` });
262
+ }));
263
+ server.registerTool('palpluss_delete_payment_channel', {
264
+ title: 'Remove a payment channel',
265
+ description: 'Remove a payment channel. Past transactions keep their record; if it was the default, another channel becomes the default. Needs the person\'s approval.',
266
+ inputSchema: { channelId: z.string().uuid() },
267
+ annotations: { title: 'Remove a payment channel', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
268
+ }, guarded(ctx, 'palpluss_delete_payment_channel', async ({ channelId }) => {
269
+ const current = (await client.getChannel(channelId));
270
+ const approval = await requireApproval(ctx, {
271
+ action: 'channel_delete',
272
+ title: `Remove payment channel "${String(current.name ?? channelId)}"`,
273
+ details: { Channel: describe(current), Default: current.isDefault ? 'Yes — another channel will become the default' : 'No' },
274
+ consequence: 'The channel can no longer be used for payments.',
275
+ });
276
+ const res = await client.deleteChannel(channelId);
277
+ return ok(res ?? { deleted: true, channelId }, { note: `Approved (${approval}). Channel removed.` });
278
+ }));
279
+ }
@@ -0,0 +1,2 @@
1
+ import { type ToolContext } from './common.js';
2
+ export declare function registerReadTools(ctx: ToolContext): void;
@@ -0,0 +1,84 @@
1
+ import { z } from 'zod';
2
+ import { guarded, ok } from './common.js';
3
+ import { maskPhone } from '../safety/redact.js';
4
+ import { GUIDE_TOPICS, guide } from './guide.js';
5
+ const READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
6
+ export function registerReadTools(ctx) {
7
+ const { server, client } = ctx;
8
+ server.registerTool('palpluss_list_transactions', {
9
+ title: 'List transactions',
10
+ description: 'List recent PalPluss transactions (STK collections and B2C payouts), newest first. Filter by type or status; page with the returned next_cursor.',
11
+ inputSchema: {
12
+ type: z.enum(['STK', 'B2C']).optional().describe('Only STK collections or only B2C payouts.'),
13
+ status: z
14
+ .enum(['PENDING', 'PROCESSING', 'SUCCESS', 'FAILED', 'REVERSED', 'CANCELLED'])
15
+ .optional()
16
+ .describe('Only transactions in this status.'),
17
+ limit: z.number().int().min(1).max(100).optional().describe('How many to return (default 20, max 100).'),
18
+ cursor: z.string().max(500).optional().describe('next_cursor from a previous page.'),
19
+ },
20
+ annotations: { title: 'List transactions', ...READ },
21
+ }, guarded(ctx, 'palpluss_list_transactions', async (args) => ok(await client.listTransactions(args))));
22
+ server.registerTool('palpluss_get_transaction', {
23
+ title: 'Get a transaction',
24
+ description: 'Get one transaction by its PalPluss transaction ID — status, amount, M-Pesa receipt and result. Use it to check whether an STK push, payout or top-up succeeded.',
25
+ inputSchema: { transactionId: z.string().uuid().describe('PalPluss transaction ID (UUID).') },
26
+ annotations: { title: 'Get a transaction', ...READ },
27
+ }, guarded(ctx, 'palpluss_get_transaction', async ({ transactionId }) => ok(await client.getTransaction(transactionId))));
28
+ server.registerTool('palpluss_get_wallet_balances', {
29
+ title: 'Wallet balances',
30
+ description: 'Balances of both PalPluss wallets: the service wallet (pays transaction fees, e.g. KES 3 per STK push) and the B2C wallet (the float that payouts are sent from).',
31
+ inputSchema: {},
32
+ annotations: { title: 'Wallet balances', ...READ },
33
+ }, guarded(ctx, 'palpluss_get_wallet_balances', async () => {
34
+ const [service, b2c] = await Promise.allSettled([client.serviceWalletBalance(), client.b2cWalletBalance()]);
35
+ const pick = (r) => r.status === 'fulfilled' ? r.value : { unavailable: true, reason: r.reason?.message ?? 'unknown' };
36
+ return ok({ serviceWallet: pick(service), b2cWallet: pick(b2c) });
37
+ }));
38
+ server.registerTool('palpluss_list_payment_channels', {
39
+ title: 'List payment channels',
40
+ description: 'List the account\'s payment channels — the Paybills and Tills customer payments are collected into. The default channel is first; STK pushes use it unless a channelId is given.',
41
+ inputSchema: {},
42
+ annotations: { title: 'List payment channels', ...READ },
43
+ }, guarded(ctx, 'palpluss_list_payment_channels', async () => ok(await client.listChannels())));
44
+ server.registerTool('palpluss_get_payment_channel', {
45
+ title: 'Get a payment channel',
46
+ description: 'Get one payment channel by ID.',
47
+ inputSchema: { channelId: z.string().uuid().describe('Payment channel ID (UUID).') },
48
+ annotations: { title: 'Get a payment channel', ...READ },
49
+ }, guarded(ctx, 'palpluss_get_payment_channel', async ({ channelId }) => ok(await client.getChannel(channelId))));
50
+ server.registerTool('palpluss_get_guardrails', {
51
+ title: 'Guardrails in force',
52
+ description: 'Show what this PalPluss MCP server is allowed to do: enabled toolsets, whether writes are switched off, how approvals work, the payout limits and how much of today\'s payout limit is left. Call this before planning payouts or channel changes.',
53
+ inputSchema: {},
54
+ annotations: { title: 'Guardrails in force', ...READ, openWorldHint: false },
55
+ }, guarded(ctx, 'palpluss_get_guardrails', async () => {
56
+ const { config } = ctx;
57
+ return ok({
58
+ toolsets: config.toolsets,
59
+ writesEnabled: !config.readOnly,
60
+ approval: config.approval === 'elicit'
61
+ ? 'Every top-up, STK push, payout and channel change asks the person to approve it in their app before anything happens.'
62
+ : 'Approval prompts are switched off (PALPLUSS_MCP_APPROVAL=none).',
63
+ limits: {
64
+ maxPayoutKes: config.limits.maxPayout,
65
+ dailyPayoutTotalKes: config.limits.dailyPayoutTotal,
66
+ dailyPayoutCount: config.limits.dailyPayoutCount,
67
+ maxTopupKes: config.limits.maxTopup,
68
+ maxStkKes: config.limits.maxStk,
69
+ payoutAllowlist: config.limits.payoutAllowlist.length
70
+ ? config.limits.payoutAllowlist.map((p) => maskPhone(p))
71
+ : 'any number',
72
+ },
73
+ payoutsToday: ctx.config.toolsets.includes('payouts') ? ctx.limiter.remaining() : 'payouts toolset is off',
74
+ }, { note: 'Limits here are enforced by this MCP server in addition to PalPluss\'s own limits.' });
75
+ }));
76
+ server.registerTool('palpluss_integration_guide', {
77
+ title: 'Integration guide',
78
+ description: 'Short, accurate guidance for building PalPluss into an app: STK push, webhooks, B2C payouts, payment channels, wallets, errors and going live. Use it before writing PalPluss integration code.',
79
+ inputSchema: { topic: z.enum(GUIDE_TOPICS).describe('Which part of the integration.') },
80
+ annotations: { title: 'Integration guide', ...READ, openWorldHint: false },
81
+ }, guarded(ctx, 'palpluss_integration_guide', async ({ topic }) => ({
82
+ content: [{ type: 'text', text: guide(topic) }],
83
+ })));
84
+ }
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@palpluss/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "PalPluss MCP server — let AI coding tools check balances, manage payment channels and wallets, collect with M-Pesa STK Push and send B2C payouts, with human approval on anything that moves money.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "bin": {
15
+ "palpluss-mcp": "dist/cli.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "EMBEDDING.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.build.json && chmod +x dist/cli.js",
24
+ "typecheck": "tsc -p tsconfig.json --noEmit",
25
+ "test": "vitest run",
26
+ "start": "node dist/cli.js",
27
+ "release:manual": "node scripts/publish.mjs"
28
+ },
29
+ "keywords": [
30
+ "palpluss",
31
+ "mpesa",
32
+ "m-pesa",
33
+ "mcp",
34
+ "model-context-protocol",
35
+ "stk-push",
36
+ "b2c",
37
+ "payments",
38
+ "kenya"
39
+ ],
40
+ "license": "MIT",
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/palpluss/palpluss-sdk.git",
47
+ "directory": "packages/mcp"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "dependencies": {
53
+ "@modelcontextprotocol/sdk": "1.32.2",
54
+ "zod": "3.25.76"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "20.19.0",
58
+ "typescript": "5.9.3",
59
+ "vitest": "3.2.4"
60
+ }
61
+ }