nextora-mcp 1.0.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.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Nextora MCP Server
2
+
3
+ Connect your AI assistant (Claude Desktop, Cursor, Cline, etc.) to Nextora to record transactions and analyze receipts by chatting.
4
+
5
+ ## Tools
6
+
7
+ | Tool | Description |
8
+ |---|---|
9
+ | `list_categories` | List available income/expense categories |
10
+ | `add_transaction` | Record a new income or expense |
11
+ | `upload_receipt` | Analyze a receipt image and auto-record the transaction |
12
+ | `get_balance` | Get income/expense/net summary for a date range |
13
+ | `list_transactions` | List recent transactions with filters |
14
+
15
+ ## Setup
16
+
17
+ ### 1. Generate an API Key
18
+
19
+ Log in to Nextora → Settings → API Keys → Generate Key.
20
+ Copy the key — it is shown **once only**.
21
+
22
+ ### 2. Configure Claude Desktop
23
+
24
+ Edit `~/.claude/settings.json` (or `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "nextora": {
30
+ "command": "npx",
31
+ "args": ["-y", "nextora-mcp"],
32
+ "env": {
33
+ "NEXTORA_API_URL": "https://your-nextora-domain.com/api",
34
+ "NEXTORA_API_KEY": "nxt_your_key_here"
35
+ }
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### 3. Or run with Docker
42
+
43
+ ```bash
44
+ docker run --rm -i \
45
+ -e NEXTORA_API_URL="https://your-domain/api" \
46
+ -e NEXTORA_API_KEY="nxt_your_key_here" \
47
+ blacksptech/nextora-mcp:latest
48
+ ```
49
+
50
+ Config ที่ user ใช้กับ Claude Desktop
51
+ {
52
+ "mcpServers": {
53
+ "nextora": {
54
+ "command": "docker",
55
+ "args": ["run", "--rm", "-i",
56
+ "-e", "NEXTORA_API_URL=https://your-domain.com/api",
57
+ "-e", "NEXTORA_API_KEY=nxt_xxx",
58
+ "blacksptech/nextora-mcp:latest"
59
+ ]
60
+ }
61
+ }
62
+ }
63
+
64
+ ## Example conversations
65
+
66
+ > "บันทึกค่ากาแฟ 80 บาท วันนี้"
67
+ > "Add a food expense of 80 baht for coffee today"
68
+
69
+ > "ส่งรูปใบเสร็จนี้แล้วบันทึกเลย /home/user/receipt.jpg"
70
+ > "Upload this receipt and record it: /Users/me/Downloads/receipt.png"
71
+
72
+ > "ยอดรายรับรายจ่ายเดือนนี้เป็นยังไง"
73
+ > "What's my income vs expense this month?"
74
+
75
+ > "แสดงรายการ 10 รายการล่าสุด"
76
+ > "Show my last 10 transactions"
@@ -0,0 +1,54 @@
1
+ import { type AxiosInstance } from "axios";
2
+ export declare function createApiClient(): AxiosInstance;
3
+ export type TransactionType = "Income" | "Expense";
4
+ export interface Category {
5
+ id: string;
6
+ name: string;
7
+ type: TransactionType;
8
+ parentId: string | null;
9
+ }
10
+ export interface Transaction {
11
+ id: string;
12
+ categoryId: string;
13
+ categoryName: string;
14
+ type: TransactionType;
15
+ amount: number;
16
+ transactionDate: string;
17
+ description: string | null;
18
+ createdAt: string;
19
+ }
20
+ export interface CreateTransactionRequest {
21
+ categoryId: string;
22
+ type: TransactionType;
23
+ amount: number;
24
+ transactionDate: string;
25
+ description?: string | null;
26
+ assetId?: string | null;
27
+ landProjectId?: string | null;
28
+ }
29
+ export interface TransactionFilter {
30
+ from?: string;
31
+ to?: string;
32
+ categoryId?: string;
33
+ }
34
+ export interface AnalyzeReceiptRequest {
35
+ imageBase64: string;
36
+ mimeType: string;
37
+ }
38
+ export interface AnalyzeReceiptResult {
39
+ amount: number | null;
40
+ type: TransactionType;
41
+ description: string | null;
42
+ transactionDate: string | null;
43
+ categoryHint: string | null;
44
+ vendor: string | null;
45
+ success: boolean;
46
+ errorMessage: string | null;
47
+ }
48
+ export interface ReportNetSummary {
49
+ totalIncome: number;
50
+ totalExpense: number;
51
+ netProfit: number;
52
+ from: string;
53
+ to: string;
54
+ }
package/dist/client.js ADDED
@@ -0,0 +1,17 @@
1
+ import axios from "axios";
2
+ export function createApiClient() {
3
+ const apiUrl = process.env.NEXTORA_API_URL;
4
+ const apiKey = process.env.NEXTORA_API_KEY;
5
+ if (!apiUrl)
6
+ throw new Error("NEXTORA_API_URL environment variable is required");
7
+ if (!apiKey)
8
+ throw new Error("NEXTORA_API_KEY environment variable is required");
9
+ return axios.create({
10
+ baseURL: apiUrl.replace(/\/$/, ""),
11
+ headers: {
12
+ "X-Api-Key": apiKey,
13
+ "Content-Type": "application/json",
14
+ },
15
+ timeout: 30_000,
16
+ });
17
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { createApiClient } from "./client.js";
5
+ import { listCategoriesSchema, listCategories } from "./tools/list_categories.js";
6
+ import { addTransactionSchema, addTransaction } from "./tools/add_transaction.js";
7
+ import { uploadReceiptSchema, uploadReceipt } from "./tools/upload_receipt.js";
8
+ import { getBalanceSchema, getBalance } from "./tools/get_balance.js";
9
+ import { listTransactionsSchema, listTransactions } from "./tools/list_transactions.js";
10
+ const server = new McpServer({
11
+ name: "nextora",
12
+ version: "1.0.0",
13
+ });
14
+ const client = createApiClient();
15
+ server.tool("list_categories", "List available transaction categories. Call this before add_transaction if you're unsure which category to use.", listCategoriesSchema.shape, async (args) => {
16
+ const text = await listCategories(client, args);
17
+ return { content: [{ type: "text", text }] };
18
+ });
19
+ server.tool("add_transaction", "Record a new income or expense transaction in Nextora.", addTransactionSchema.shape, async (args) => {
20
+ const text = await addTransaction(client, args);
21
+ return { content: [{ type: "text", text }] };
22
+ });
23
+ server.tool("upload_receipt", "Analyze a receipt image file and automatically record the transaction. Requires an AI provider configured in Nextora Settings.", uploadReceiptSchema.shape, async (args) => {
24
+ const text = await uploadReceipt(client, args);
25
+ return { content: [{ type: "text", text }] };
26
+ });
27
+ server.tool("get_balance", "Get income, expense and net profit summary for a date range. Defaults to the current month.", getBalanceSchema.shape, async (args) => {
28
+ const text = await getBalance(client, args);
29
+ return { content: [{ type: "text", text }] };
30
+ });
31
+ server.tool("list_transactions", "List recent transactions with optional date range and type filters.", listTransactionsSchema.shape, async (args) => {
32
+ const text = await listTransactions(client, args);
33
+ return { content: [{ type: "text", text }] };
34
+ });
35
+ async function main() {
36
+ const transport = new StdioServerTransport();
37
+ await server.connect(transport);
38
+ // Startup message goes to stderr so it doesn't pollute the MCP JSON-RPC stdio stream
39
+ process.stderr.write("Nextora MCP server running (stdio)\n");
40
+ }
41
+ main().catch((err) => {
42
+ process.stderr.write(`Fatal error: ${err}\n`);
43
+ process.exit(1);
44
+ });
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import type { AxiosInstance } from "axios";
3
+ export declare const addTransactionSchema: z.ZodObject<{
4
+ type: z.ZodEnum<["Income", "Expense"]>;
5
+ amount: z.ZodNumber;
6
+ categoryName: z.ZodString;
7
+ description: z.ZodOptional<z.ZodString>;
8
+ transactionDate: z.ZodOptional<z.ZodString>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ type: "Income" | "Expense";
11
+ amount: number;
12
+ categoryName: string;
13
+ description?: string | undefined;
14
+ transactionDate?: string | undefined;
15
+ }, {
16
+ type: "Income" | "Expense";
17
+ amount: number;
18
+ categoryName: string;
19
+ description?: string | undefined;
20
+ transactionDate?: string | undefined;
21
+ }>;
22
+ export declare function addTransaction(client: AxiosInstance, args: z.infer<typeof addTransactionSchema>): Promise<string>;
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ export const addTransactionSchema = z.object({
3
+ type: z.enum(["Income", "Expense"]).describe("Income or Expense"),
4
+ amount: z.number().positive().describe("Amount in the account's currency (positive number)"),
5
+ categoryName: z.string().describe("Category name (partial match OK). Call list_categories first if unsure."),
6
+ description: z.string().optional().describe("Optional note about this transaction"),
7
+ transactionDate: z.string().optional().describe("Date as YYYY-MM-DD. Defaults to today if omitted."),
8
+ });
9
+ export async function addTransaction(client, args) {
10
+ // 1. Find the best matching category
11
+ const catResponse = await client.get("/transaction-categories");
12
+ const allCategories = catResponse.data.filter((c) => c.type === args.type);
13
+ const nameLower = args.categoryName.toLowerCase();
14
+ let category = allCategories.find((c) => c.name.toLowerCase() === nameLower);
15
+ if (!category) {
16
+ category = allCategories.find((c) => c.name.toLowerCase().includes(nameLower));
17
+ }
18
+ if (!category) {
19
+ // Fallback: use first category of matching type
20
+ category = allCategories[0];
21
+ }
22
+ if (!category) {
23
+ const available = catResponse.data.map((c) => `${c.name} (${c.type})`).join(", ");
24
+ return `No ${args.type} category found matching "${args.categoryName}". Available categories: ${available}. Please call list_categories and retry.`;
25
+ }
26
+ // 2. Determine date
27
+ const transactionDate = args.transactionDate ?? new Date().toISOString().slice(0, 10);
28
+ // 3. Create transaction
29
+ const req = {
30
+ categoryId: category.id,
31
+ type: args.type,
32
+ amount: args.amount,
33
+ transactionDate,
34
+ description: args.description ?? null,
35
+ };
36
+ const response = await client.post("/transactions", req);
37
+ const tx = response.data;
38
+ return [
39
+ `✅ Transaction recorded successfully!`,
40
+ ``,
41
+ ` ID: ${tx.id}`,
42
+ ` Type: ${tx.type}`,
43
+ ` Amount: ${tx.amount.toLocaleString()}`,
44
+ ` Category: ${tx.categoryName}`,
45
+ ` Date: ${tx.transactionDate}`,
46
+ tx.description ? ` Description: ${tx.description}` : null,
47
+ ]
48
+ .filter(Boolean)
49
+ .join("\n");
50
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import type { AxiosInstance } from "axios";
3
+ export declare const getBalanceSchema: z.ZodObject<{
4
+ from: z.ZodOptional<z.ZodString>;
5
+ to: z.ZodOptional<z.ZodString>;
6
+ }, "strip", z.ZodTypeAny, {
7
+ from?: string | undefined;
8
+ to?: string | undefined;
9
+ }, {
10
+ from?: string | undefined;
11
+ to?: string | undefined;
12
+ }>;
13
+ export declare function getBalance(client: AxiosInstance, args: z.infer<typeof getBalanceSchema>): Promise<string>;
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ export const getBalanceSchema = z.object({
3
+ from: z.string().optional().describe("Start date YYYY-MM-DD (default: first day of current month)"),
4
+ to: z.string().optional().describe("End date YYYY-MM-DD (default: today)"),
5
+ });
6
+ export async function getBalance(client, args) {
7
+ const today = new Date();
8
+ const defaultFrom = new Date(today.getFullYear(), today.getMonth(), 1)
9
+ .toISOString()
10
+ .slice(0, 10);
11
+ const defaultTo = today.toISOString().slice(0, 10);
12
+ const from = args.from ?? defaultFrom;
13
+ const to = args.to ?? defaultTo;
14
+ const response = await client.get("/reports/net-summary", {
15
+ params: { from, to },
16
+ });
17
+ const s = response.data;
18
+ const fmt = (n) => n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
19
+ const netLabel = s.netProfit >= 0 ? "Net Profit ✅" : "Net Loss ⚠️";
20
+ return [
21
+ `💰 Financial Summary (${s.from} → ${s.to})`,
22
+ ``,
23
+ ` Total Income: ${fmt(s.totalIncome)}`,
24
+ ` Total Expense: ${fmt(s.totalExpense)}`,
25
+ ` ${netLabel}: ${fmt(Math.abs(s.netProfit))}`,
26
+ ].join("\n");
27
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ import type { AxiosInstance } from "axios";
3
+ export declare const listCategoriesSchema: z.ZodObject<{
4
+ type: z.ZodDefault<z.ZodOptional<z.ZodEnum<["Income", "Expense", "All"]>>>;
5
+ }, "strip", z.ZodTypeAny, {
6
+ type: "Income" | "Expense" | "All";
7
+ }, {
8
+ type?: "Income" | "Expense" | "All" | undefined;
9
+ }>;
10
+ export declare function listCategories(client: AxiosInstance, args: z.infer<typeof listCategoriesSchema>): Promise<string>;
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ export const listCategoriesSchema = z.object({
3
+ type: z.enum(["Income", "Expense", "All"]).optional().default("All")
4
+ .describe("Filter by transaction type"),
5
+ });
6
+ export async function listCategories(client, args) {
7
+ const response = await client.get("/transaction-categories");
8
+ let categories = response.data;
9
+ if (args.type !== "All") {
10
+ categories = categories.filter((c) => c.type === args.type);
11
+ }
12
+ if (categories.length === 0) {
13
+ return `No ${args.type === "All" ? "" : args.type + " "}categories found.`;
14
+ }
15
+ const lines = categories.map((c) => `• [${c.id}] ${c.name} (${c.type})${c.parentId ? " [sub-category]" : ""}`);
16
+ return `Found ${categories.length} categor${categories.length === 1 ? "y" : "ies"}:\n\n${lines.join("\n")}`;
17
+ }
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import type { AxiosInstance } from "axios";
3
+ export declare const listTransactionsSchema: z.ZodObject<{
4
+ from: z.ZodOptional<z.ZodString>;
5
+ to: z.ZodOptional<z.ZodString>;
6
+ limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
7
+ type: z.ZodDefault<z.ZodOptional<z.ZodEnum<["Income", "Expense", "All"]>>>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ type: "Income" | "Expense" | "All";
10
+ limit: number;
11
+ from?: string | undefined;
12
+ to?: string | undefined;
13
+ }, {
14
+ type?: "Income" | "Expense" | "All" | undefined;
15
+ from?: string | undefined;
16
+ to?: string | undefined;
17
+ limit?: number | undefined;
18
+ }>;
19
+ export declare function listTransactions(client: AxiosInstance, args: z.infer<typeof listTransactionsSchema>): Promise<string>;
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ export const listTransactionsSchema = z.object({
3
+ from: z.string().optional().describe("Start date YYYY-MM-DD"),
4
+ to: z.string().optional().describe("End date YYYY-MM-DD"),
5
+ limit: z.number().int().min(1).max(50).optional().default(20)
6
+ .describe("Maximum number of results to return (1–50, default 20)"),
7
+ type: z.enum(["Income", "Expense", "All"]).optional().default("All")
8
+ .describe("Filter by transaction type"),
9
+ });
10
+ export async function listTransactions(client, args) {
11
+ const params = {};
12
+ if (args.from)
13
+ params.from = args.from;
14
+ if (args.to)
15
+ params.to = args.to;
16
+ const response = await client.get("/transactions", { params });
17
+ let transactions = response.data;
18
+ if (args.type !== "All") {
19
+ transactions = transactions.filter((t) => t.type === args.type);
20
+ }
21
+ // Take most recent N
22
+ const sliced = transactions.slice(-args.limit).reverse();
23
+ if (sliced.length === 0) {
24
+ return `No transactions found${args.from ? ` from ${args.from}` : ""}${args.to ? ` to ${args.to}` : ""}.`;
25
+ }
26
+ const fmt = (n) => n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
27
+ const lines = sliced.map((t) => {
28
+ const sign = t.type === "Income" ? "+" : "-";
29
+ const desc = t.description ? ` — ${t.description}` : "";
30
+ return ` ${t.transactionDate} ${sign}${fmt(t.amount).padStart(12)} ${t.categoryName}${desc}`;
31
+ });
32
+ const totalIncome = sliced.filter((t) => t.type === "Income").reduce((s, t) => s + t.amount, 0);
33
+ const totalExpense = sliced.filter((t) => t.type === "Expense").reduce((s, t) => s + t.amount, 0);
34
+ return [
35
+ `📊 Last ${sliced.length} transaction${sliced.length !== 1 ? "s" : ""}:`,
36
+ ``,
37
+ ...lines,
38
+ ``,
39
+ ` Income: +${fmt(totalIncome)} | Expense: -${fmt(totalExpense)} | Net: ${fmt(totalIncome - totalExpense)}`,
40
+ ].join("\n");
41
+ }
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import type { AxiosInstance } from "axios";
3
+ export declare const uploadReceiptSchema: z.ZodObject<{
4
+ imagePath: z.ZodString;
5
+ autoRecord: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
6
+ descriptionOverride: z.ZodOptional<z.ZodString>;
7
+ amountOverride: z.ZodOptional<z.ZodNumber>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ imagePath: string;
10
+ autoRecord: boolean;
11
+ descriptionOverride?: string | undefined;
12
+ amountOverride?: number | undefined;
13
+ }, {
14
+ imagePath: string;
15
+ autoRecord?: boolean | undefined;
16
+ descriptionOverride?: string | undefined;
17
+ amountOverride?: number | undefined;
18
+ }>;
19
+ export declare function uploadReceipt(client: AxiosInstance, args: z.infer<typeof uploadReceiptSchema>): Promise<string>;
@@ -0,0 +1,89 @@
1
+ import { z } from "zod";
2
+ import { readFileSync } from "fs";
3
+ import { extname } from "path";
4
+ export const uploadReceiptSchema = z.object({
5
+ imagePath: z.string().describe("Absolute path to the receipt image file (JPG, PNG, WEBP, or PDF)"),
6
+ autoRecord: z.boolean().optional().default(true).describe("Automatically create the transaction after analysis. Set to false to preview only."),
7
+ descriptionOverride: z.string().optional().describe("Override the AI-suggested description"),
8
+ amountOverride: z.number().positive().optional().describe("Override the AI-detected amount"),
9
+ });
10
+ const MIME_MAP = {
11
+ ".jpg": "image/jpeg",
12
+ ".jpeg": "image/jpeg",
13
+ ".png": "image/png",
14
+ ".webp": "image/webp",
15
+ ".gif": "image/gif",
16
+ ".pdf": "application/pdf",
17
+ };
18
+ export async function uploadReceipt(client, args) {
19
+ // 1. Read image file
20
+ let imageBuffer;
21
+ try {
22
+ imageBuffer = readFileSync(args.imagePath);
23
+ }
24
+ catch {
25
+ return `❌ Could not read file at "${args.imagePath}". Please check the path exists and is accessible.`;
26
+ }
27
+ const ext = extname(args.imagePath).toLowerCase();
28
+ const mimeType = MIME_MAP[ext] ?? "image/jpeg";
29
+ const imageBase64 = imageBuffer.toString("base64");
30
+ // 2. Analyze via API
31
+ const analyzeReq = { imageBase64, mimeType };
32
+ const analyzeRes = await client.post("/transactions/analyze-image", analyzeReq);
33
+ const result = analyzeRes.data;
34
+ if (!result.success) {
35
+ return `❌ Receipt analysis failed: ${result.errorMessage}\n\nTip: Make sure an AI provider (OpenAI, Claude, or Gemini) is configured in Settings.`;
36
+ }
37
+ const amount = args.amountOverride ?? result.amount;
38
+ const description = args.descriptionOverride ?? (result.description ?? result.vendor ?? null);
39
+ const type = result.type ?? "Expense";
40
+ const dateStr = result.transactionDate ?? new Date().toISOString().slice(0, 10);
41
+ // Build preview
42
+ const preview = [
43
+ `📋 Receipt Analysis Result:`,
44
+ ``,
45
+ ` Type: ${type}`,
46
+ ` Amount: ${amount?.toLocaleString() ?? "(not detected)"}`,
47
+ ` Date: ${dateStr}`,
48
+ result.vendor ? ` Vendor: ${result.vendor}` : null,
49
+ description ? ` Description: ${description}` : null,
50
+ result.categoryHint ? ` Category: ${result.categoryHint} (suggested)` : null,
51
+ ]
52
+ .filter(Boolean)
53
+ .join("\n");
54
+ if (!args.autoRecord) {
55
+ return `${preview}\n\n💡 autoRecord is false — call again with autoRecord: true to save this transaction.`;
56
+ }
57
+ if (!amount || amount <= 0) {
58
+ return `${preview}\n\n❌ Could not determine amount from receipt. Please call add_transaction manually with the correct amount.`;
59
+ }
60
+ // 3. Find matching category
61
+ const catResponse = await client.get("/transaction-categories");
62
+ const typeCategories = catResponse.data.filter((c) => c.type === type);
63
+ let category;
64
+ if (result.categoryHint) {
65
+ const hint = result.categoryHint.toLowerCase();
66
+ category = typeCategories.find((c) => c.name.toLowerCase().includes(hint));
67
+ }
68
+ category ??= typeCategories[0];
69
+ if (!category) {
70
+ return `${preview}\n\n❌ No ${type} categories configured. Please create one in Settings first.`;
71
+ }
72
+ // 4. Create transaction
73
+ const req = {
74
+ categoryId: category.id,
75
+ type,
76
+ amount,
77
+ transactionDate: dateStr,
78
+ description,
79
+ };
80
+ const txRes = await client.post("/transactions", req);
81
+ const tx = txRes.data;
82
+ return [
83
+ preview,
84
+ ``,
85
+ `✅ Transaction recorded!`,
86
+ ` ID: ${tx.id}`,
87
+ ` Category: ${tx.categoryName}`,
88
+ ].join("\n");
89
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "nextora-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Nextora MCP Server — record transactions and analyze receipts via AI",
5
+ "type": "module",
6
+ "files": ["dist"],
7
+ "bin": {
8
+ "nextora-mcp": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx src/index.ts",
13
+ "start": "node dist/index.js"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.15.0",
17
+ "axios": "^1.9.0",
18
+ "zod": "^3.24.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.0.0",
22
+ "tsx": "^4.19.0",
23
+ "typescript": "^5.8.0"
24
+ }
25
+ }
@@ -0,0 +1,294 @@
1
+ # วิธีใช้งาน Nextora MCP Server กับ AI ต่างๆ
2
+
3
+ ## ขั้นตอนแรก: สร้าง API Key
4
+
5
+ 1. Login เข้า Nextora → **Settings → API Keys**
6
+ 2. กด **"สร้าง Key"** → ตั้งชื่อ เช่น `My Claude Desktop`
7
+ 3. **Copy key ที่แสดง** — จะแสดงครั้งเดียวเท่านั้น (รูปแบบ: `nxt_xxxxxxxxxx...`)
8
+ 4. จดค่า API URL ของ Nextora ไว้ด้วย เช่น `https://your-domain.com/api`
9
+
10
+ ---
11
+
12
+ ## 1. Claude Desktop (แนะนำ — รองรับ native)
13
+
14
+ Claude Desktop คือแอปบน Mac/Windows ที่รองรับ MCP โดยตรง
15
+
16
+ ### ขั้นตอน
17
+
18
+ **Mac:** เปิดไฟล์ `~/Library/Application Support/Claude/claude_desktop_config.json`
19
+ **Windows:** เปิดไฟล์ `%APPDATA%\Claude\claude_desktop_config.json`
20
+
21
+ เพิ่ม config นี้:
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "nextora": {
27
+ "command": "npx",
28
+ "args": ["-y", "nextora-mcp"],
29
+ "env": {
30
+ "NEXTORA_API_URL": "https://your-domain.com/api",
31
+ "NEXTORA_API_KEY": "nxt_your_key_here"
32
+ }
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ > ถ้าไม่มี `npx` หรือ Node.js → ดาวน์โหลดที่ [nodejs.org](https://nodejs.org) (LTS)
39
+
40
+ **Restart Claude Desktop** แล้วจะเห็น 🔧 icon ด้านล่าง chat — หมายความว่า tools พร้อมใช้
41
+
42
+ ### ลองใช้
43
+
44
+ ```
45
+ "บันทึกค่ากาแฟ 80 บาท วันนี้"
46
+ "ส่งรูปใบเสร็จนี้แล้วบันทึกด้วย: /Users/me/Downloads/receipt.jpg"
47
+ "ยอดรายรับรายจ่ายเดือนนี้เป็นเท่าไหร่"
48
+ "แสดงรายการล่าสุด 10 รายการ"
49
+ ```
50
+
51
+ ---
52
+
53
+ ## 2. Cursor IDE
54
+
55
+ Cursor เป็น Code Editor ที่รองรับ MCP tools
56
+
57
+ ### ขั้นตอน
58
+
59
+ เปิด **Cursor Settings** → ค้นหา **MCP** → เพิ่ม server ใหม่:
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "nextora": {
65
+ "command": "npx",
66
+ "args": ["-y", "nextora-mcp"],
67
+ "env": {
68
+ "NEXTORA_API_URL": "https://your-domain.com/api",
69
+ "NEXTORA_API_KEY": "nxt_your_key_here"
70
+ }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ หรือเพิ่มที่ไฟล์ `.cursor/mcp.json` ใน project root:
77
+
78
+ ```json
79
+ {
80
+ "mcpServers": {
81
+ "nextora": {
82
+ "command": "npx",
83
+ "args": ["-y", "nextora-mcp"],
84
+ "env": {
85
+ "NEXTORA_API_URL": "https://your-domain.com/api",
86
+ "NEXTORA_API_KEY": "nxt_your_key_here"
87
+ }
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ เปิด Cursor Chat (Cmd/Ctrl+L) → พิมพ์คำสั่งได้เลย
94
+
95
+ ---
96
+
97
+ ## 3. Cline (VS Code Extension)
98
+
99
+ Cline คือ VS Code extension ที่ทรงพลัง รองรับ MCP tools ครบ
100
+
101
+ ### ขั้นตอน
102
+
103
+ 1. ติดตั้ง extension **Cline** จาก VS Code Marketplace
104
+ 2. เปิด Cline Panel (icon ด้านซ้าย)
105
+ 3. กด **Settings icon** → **MCP Servers** → **Edit Config**
106
+ 4. เพิ่ม:
107
+
108
+ ```json
109
+ {
110
+ "mcpServers": {
111
+ "nextora": {
112
+ "command": "npx",
113
+ "args": ["-y", "nextora-mcp"],
114
+ "env": {
115
+ "NEXTORA_API_URL": "https://your-domain.com/api",
116
+ "NEXTORA_API_KEY": "nxt_your_key_here"
117
+ }
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ 5. Save → กด **Restart MCP Servers**
124
+ 6. เห็น `nextora` ใน MCP Servers list → พร้อมใช้
125
+
126
+ > **Cline รองรับ OpenAI, Claude, Gemini, Ollama ฯลฯ** — ตั้งค่า AI provider ใน Cline Settings ได้อิสระ
127
+
128
+ ---
129
+
130
+ ## 4. Continue.dev (VS Code / JetBrains)
131
+
132
+ Continue เป็น open-source AI coding assistant รองรับ MCP
133
+
134
+ ### ขั้นตอน
135
+
136
+ เปิดไฟล์ `~/.continue/config.json` แล้วเพิ่ม:
137
+
138
+ ```json
139
+ {
140
+ "experimental": {
141
+ "modelContextProtocolServers": [
142
+ {
143
+ "transport": {
144
+ "type": "stdio",
145
+ "command": "npx",
146
+ "args": ["-y", "nextora-mcp"],
147
+ "env": {
148
+ "NEXTORA_API_URL": "https://your-domain.com/api",
149
+ "NEXTORA_API_KEY": "nxt_your_key_here"
150
+ }
151
+ }
152
+ }
153
+ ]
154
+ }
155
+ }
156
+ ```
157
+
158
+ ---
159
+
160
+ ## 5. ChatGPT / OpenAI (ผ่าน Bridge)
161
+
162
+ OpenAI ยังไม่รองรับ MCP โดยตรงใน ChatGPT web app ณ ขณะนี้ (กรกฎาคม 2026) แต่มีวิธีอ้อม:
163
+
164
+ ### Option A: ใช้ผ่าน OpenAI API + MCP-to-OpenAI Bridge
165
+
166
+ ถ้าเขียน code เอง สามารถใช้ library เชื่อม MCP กับ OpenAI Function Calling:
167
+
168
+ ```typescript
169
+ import OpenAI from "openai";
170
+ // แปลง MCP tools เป็น OpenAI function definitions แล้วส่ง tool results กลับ
171
+ ```
172
+
173
+ ### Option B: ใช้ Cline ที่ตั้งค่า OpenAI เป็น provider
174
+
175
+ Cline รองรับทั้ง MCP tools และ OpenAI API — ตั้งค่า Cline ให้ใช้ OpenAI แล้ว connect Nextora MCP เข้าไป:
176
+
177
+ 1. ติดตั้ง Cline
178
+ 2. Settings → AI Provider → เลือก **OpenAI**, ใส่ API Key ของ OpenAI
179
+ 3. เพิ่ม Nextora MCP server ตามขั้นตอนใน [ส่วนที่ 3]
180
+
181
+ > วิธีนี้ใช้ OpenAI เป็น brain แต่ได้ Nextora tools ครบ
182
+
183
+ ---
184
+
185
+ ## 6. Open WebUI (Self-hosted)
186
+
187
+ สำหรับคนที่รัน Open WebUI (Ollama / Local LLM):
188
+
189
+ ```json
190
+ {
191
+ "tools": [
192
+ {
193
+ "type": "mcp",
194
+ "server": {
195
+ "type": "stdio",
196
+ "command": "npx",
197
+ "args": ["-y", "nextora-mcp"],
198
+ "env": {
199
+ "NEXTORA_API_URL": "https://your-domain.com/api",
200
+ "NEXTORA_API_KEY": "nxt_your_key_here"
201
+ }
202
+ }
203
+ }
204
+ ]
205
+ }
206
+ ```
207
+
208
+ ---
209
+
210
+ ## 7. Docker Run (ถ้าไม่ต้องการติดตั้ง Node.js)
211
+
212
+ ```bash
213
+ # ใช้ image จาก Docker Hub
214
+ docker run --rm -i \
215
+ -e NEXTORA_API_URL="https://your-domain.com/api" \
216
+ -e NEXTORA_API_KEY="nxt_your_key_here" \
217
+ blacksptech/nextora-mcp:latest
218
+ ```
219
+
220
+ จากนั้นเปลี่ยน command ใน config เป็น:
221
+
222
+ ```json
223
+ {
224
+ "mcpServers": {
225
+ "nextora": {
226
+ "command": "docker",
227
+ "args": [
228
+ "run", "--rm", "-i",
229
+ "-e", "NEXTORA_API_URL=https://your-domain.com/api",
230
+ "-e", "NEXTORA_API_KEY=nxt_your_key_here",
231
+ "blacksptech/nextora-mcp:latest"
232
+ ]
233
+ }
234
+ }
235
+ }
236
+ ```
237
+
238
+ ---
239
+
240
+ ## 8. ตัวอย่างบทสนทนา
241
+
242
+ ### บันทึกรายจ่าย
243
+ ```
244
+ You: บันทึกค่าน้ำมัน 500 บาท เมื่อวานนี้
245
+ AI: ✅ บันทึกแล้ว
246
+ Type: Expense
247
+ Amount: 500
248
+ Category: Transport
249
+ Date: 2026-07-03
250
+ ```
251
+
252
+ ### วิเคราะห์ใบเสร็จ
253
+ ```
254
+ You: ส่งรูปใบเสร็จนี้แล้วบันทึกด้วย: /Users/me/receipt.png
255
+ AI: 📋 ผลการวิเคราะห์:
256
+ Type: Expense
257
+ Amount: 1,250
258
+ Vendor: ห้างสรรพสินค้า ABC
259
+ Date: 2026-07-04
260
+ ✅ บันทึกแล้ว (Category: Shopping)
261
+ ```
262
+
263
+ ### ดูสรุปการเงิน
264
+ ```
265
+ You: ยอดเดือนนี้เป็นยังไง
266
+ AI: 💰 สรุปเดือนกรกฎาคม 2026
267
+ รายรับ: 45,000.00
268
+ รายจ่าย: 32,450.00
269
+ กำไรสุทธิ: 12,550.00 ✅
270
+ ```
271
+
272
+ ### ดูรายการล่าสุด
273
+ ```
274
+ You: แสดง 5 รายการล่าสุด
275
+ AI: 📊 5 รายการล่าสุด:
276
+ 2026-07-04 -1,250.00 Shopping ห้างฯ ABC
277
+ 2026-07-04 -500.00 Transport น้ำมัน
278
+ 2026-07-03 -80.00 Food กาแฟ
279
+ 2026-07-02 +45,000.00 Salary เงินเดือน
280
+ 2026-07-01 -350.00 Utilities ค่าไฟ
281
+ ```
282
+
283
+ ---
284
+
285
+ ## 9. Troubleshooting
286
+
287
+ | ปัญหา | สาเหตุ | วิธีแก้ |
288
+ |---|---|---|
289
+ | `NEXTORA_API_URL is required` | ไม่ได้ set env var | ตรวจสอบ config ว่า env ครบ |
290
+ | `Invalid API key` | Key ผิดหรือถูก revoke | ไปที่ Settings → API Keys → สร้างใหม่ |
291
+ | `No AI API key configured` | ใช้ upload_receipt แต่ยังไม่ได้ set AI provider | Settings → Receipt scanning → เพิ่ม OpenAI/Claude key |
292
+ | Tool ไม่ขึ้นใน AI client | Restart client หลัง save config | ปิด-เปิด Claude Desktop / VS Code ใหม่ |
293
+ | `npx` ไม่ทำงาน | ไม่มี Node.js | ติดตั้ง Node.js LTS จาก nodejs.org |
294
+ | Timeout | Network ช้าหรือ Nextora ไม่ตอบ | ตรวจ NEXTORA_API_URL ว่าเข้าถึงได้ |
@@ -0,0 +1,266 @@
1
+ # Nextora MCP Server — วิธีการเขียนและโครงสร้าง
2
+
3
+ ## 1. MCP คืออะไร
4
+
5
+ **Model Context Protocol (MCP)** คือมาตรฐานเปิด (Open Standard) ที่ Anthropic พัฒนาขึ้น เพื่อให้ AI Assistant สื่อสารกับระบบภายนอกได้แบบ standardized ทำหน้าที่คล้าย USB-C — "port" เดียว เชื่อมได้ทุก AI ที่รองรับ
6
+
7
+ ```
8
+ AI Client (Claude Desktop / Cursor / Cline)
9
+
10
+ │ JSON-RPC 2.0 over stdio / HTTP-SSE
11
+
12
+ MCP Server (nextora-mcp)
13
+
14
+ │ HTTP REST + X-Api-Key
15
+
16
+ Nextora API (.NET 10)
17
+
18
+
19
+ PostgreSQL
20
+ ```
21
+
22
+ ---
23
+
24
+ ## 2. โครงสร้างโปรเจกต์
25
+
26
+ ```
27
+ mcp-server/
28
+ ├── src/
29
+ │ ├── index.ts ← Entry point — สร้าง server + register tools + connect transport
30
+ │ ├── client.ts ← Axios HTTP client wrapper + TypeScript types
31
+ │ └── tools/
32
+ │ ├── list_categories.ts ← Tool: list_categories
33
+ │ ├── add_transaction.ts ← Tool: add_transaction
34
+ │ ├── upload_receipt.ts ← Tool: upload_receipt
35
+ │ ├── get_balance.ts ← Tool: get_balance
36
+ │ └── list_transactions.ts ← Tool: list_transactions
37
+ ├── package.json
38
+ ├── tsconfig.json
39
+ └── Dockerfile
40
+ ```
41
+
42
+ ---
43
+
44
+ ## 3. เทคโนโลยีที่ใช้
45
+
46
+ | Package | เวอร์ชัน | หน้าที่ |
47
+ |---|---|---|
48
+ | `@modelcontextprotocol/sdk` | ^1.15 | MCP SDK (server, transport, tool registry) |
49
+ | `axios` | ^1.9 | HTTP client ยิง REST API ไปที่ Nextora |
50
+ | `zod` | ^3.24 | Schema validation สำหรับ tool arguments |
51
+ | `typescript` | ^5.8 | Type safety ทั้ง project |
52
+ | `tsx` | ^4.19 | Dev runner (run TypeScript โดยตรง) |
53
+
54
+ **Runtime**: Node.js 22+, ES Module (`"type": "module"`)
55
+
56
+ ---
57
+
58
+ ## 4. วิธีการเขียน MCP Server
59
+
60
+ ### 4.1 สร้าง McpServer instance
61
+
62
+ ```typescript
63
+ // src/index.ts
64
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
65
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
66
+
67
+ const server = new McpServer({
68
+ name: "nextora", // ชื่อที่ AI client เห็น
69
+ version: "1.0.0",
70
+ });
71
+ ```
72
+
73
+ ### 4.2 Register Tool
74
+
75
+ แต่ละ tool ต้องมี 4 ส่วน:
76
+
77
+ ```typescript
78
+ server.tool(
79
+ "tool_name", // 1. ชื่อ tool (snake_case)
80
+ "Description for AI to read", // 2. คำอธิบาย — AI ใช้ตัดสินใจว่าจะเรียก tool ไหน
81
+ schemaObject.shape, // 3. Zod schema ของ arguments
82
+ async (args) => { // 4. Handler function
83
+ const text = await doSomething(args);
84
+ return { content: [{ type: "text", text }] };
85
+ }
86
+ );
87
+ ```
88
+
89
+ ### 4.3 Schema ด้วย Zod
90
+
91
+ ```typescript
92
+ // src/tools/add_transaction.ts
93
+ import { z } from "zod";
94
+
95
+ export const addTransactionSchema = z.object({
96
+ type: z.enum(["Income", "Expense"]).describe("Income or Expense"),
97
+ amount: z.number().positive().describe("Amount (positive number)"),
98
+ categoryName: z.string().describe("Category name, partial match OK"),
99
+ description: z.string().optional().describe("Optional note"),
100
+ transactionDate: z.string().optional().describe("YYYY-MM-DD, defaults to today"),
101
+ });
102
+ ```
103
+
104
+ > `.describe(...)` คือสิ่งที่ AI อ่านเพื่อเข้าใจว่า field นี้ใส่อะไร — เขียนให้ชัด
105
+
106
+ ### 4.4 API Client (Authentication)
107
+
108
+ ```typescript
109
+ // src/client.ts
110
+ import axios from "axios";
111
+
112
+ export function createApiClient() {
113
+ return axios.create({
114
+ baseURL: process.env.NEXTORA_API_URL,
115
+ headers: {
116
+ "X-Api-Key": process.env.NEXTORA_API_KEY, // API Key auth
117
+ "Content-Type": "application/json",
118
+ },
119
+ timeout: 30_000,
120
+ });
121
+ }
122
+ ```
123
+
124
+ API Key ถูกส่งผ่าน header `X-Api-Key` ทุก request (ไม่ใช่ JWT Bearer)
125
+
126
+ ### 4.5 Tool Logic ตัวอย่าง: add_transaction
127
+
128
+ ```typescript
129
+ export async function addTransaction(client, args) {
130
+ // 1. หา category จากชื่อ (partial match)
131
+ const cats = await client.get("/transaction-categories");
132
+ const category = cats.data.find(c =>
133
+ c.type === args.type &&
134
+ c.name.toLowerCase().includes(args.categoryName.toLowerCase())
135
+ );
136
+
137
+ // 2. สร้าง transaction
138
+ const response = await client.post("/transactions", {
139
+ categoryId: category.id,
140
+ type: args.type,
141
+ amount: args.amount,
142
+ transactionDate: args.transactionDate ?? today,
143
+ description: args.description,
144
+ });
145
+
146
+ // 3. return plain text สำหรับ AI อ่าน
147
+ return `✅ Transaction recorded!\n ID: ${response.data.id}\n Amount: ${args.amount}`;
148
+ }
149
+ ```
150
+
151
+ ### 4.6 Transport: stdio
152
+
153
+ ```typescript
154
+ async function main() {
155
+ const transport = new StdioServerTransport();
156
+ await server.connect(transport);
157
+ process.stderr.write("Nextora MCP server running\n");
158
+ // stderr = ข้อความ log, ไม่รบกวน JSON-RPC stream ที่ไปที่ stdout
159
+ }
160
+ ```
161
+
162
+ **Stdio transport** = AI client launch process นี้ขึ้นมา แล้วคุยกันผ่าน stdin/stdout เป็น JSON-RPC 2.0
163
+
164
+ ---
165
+
166
+ ## 5. Tools ที่ implement
167
+
168
+ ### 5.1 `list_categories`
169
+ - เรียก `GET /api/transaction-categories`
170
+ - รองรับ filter by type (Income / Expense / All)
171
+ - Return: รายการ category พร้อม ID
172
+
173
+ ### 5.2 `add_transaction`
174
+ - รับ: type, amount, categoryName (partial), description?, date?
175
+ - Logic: หา category → สร้าง transaction
176
+ - Smart matching: ชื่อ category ไม่ต้องตรงพอดี
177
+
178
+ ### 5.3 `upload_receipt`
179
+ - รับ: imagePath (absolute path ในเครื่อง AI client)
180
+ - อ่านไฟล์ → base64 → ส่งไป `POST /api/transactions/analyze-image`
181
+ - API วิเคราะห์ด้วย Claude / OpenAI / Gemini Vision
182
+ - ถ้า `autoRecord: true` (default) → สร้าง transaction อัตโนมัติ
183
+
184
+ ### 5.4 `get_balance`
185
+ - เรียก `GET /api/reports/net-summary?from=&to=`
186
+ - Default: เดือนปัจจุบัน
187
+ - Return: รายรับ / รายจ่าย / กำไร-ขาดทุน
188
+
189
+ ### 5.5 `list_transactions`
190
+ - เรียก `GET /api/transactions?from=&to=`
191
+ - รองรับ filter by date, type, limit
192
+ - Return: ตาราง + summary
193
+
194
+ ---
195
+
196
+ ## 6. Backend ที่เพิ่มเพื่อรองรับ MCP
197
+
198
+ ### 6.1 API Key Authentication
199
+
200
+ เพิ่ม scheme ใหม่ใน ASP.NET Core:
201
+
202
+ ```
203
+ Header: X-Api-Key: nxt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
204
+ ```
205
+
206
+ `ApiKeyAuthenticationHandler` จะ:
207
+ 1. Hash raw key ด้วย SHA-256
208
+ 2. ค้นหาใน `api_keys` table
209
+ 3. Load user + permissions จาก DB
210
+ 4. Build `ClaimsPrincipal` เหมือน JWT (ใช้ policy เดิมได้เลย)
211
+
212
+ ### 6.2 Receipt Analysis Endpoint
213
+
214
+ `POST /api/transactions/analyze-image`
215
+
216
+ ```json
217
+ {
218
+ "imageBase64": "...",
219
+ "mimeType": "image/jpeg"
220
+ }
221
+ ```
222
+
223
+ ส่งต่อไปยัง AI provider ที่ org configure ไว้:
224
+ - **Claude**: `POST https://api.anthropic.com/v1/messages` model `claude-haiku-4-5-20251001`
225
+ - **OpenAI**: `POST https://api.openai.com/v1/chat/completions` model `gpt-4o-mini`
226
+ - **Gemini**: `POST https://generativelanguage.googleapis.com/...gemini-1.5-flash`
227
+
228
+ ---
229
+
230
+ ## 7. Build & Run
231
+
232
+ ```bash
233
+ # Install dependencies
234
+ cd mcp-server
235
+ npm install
236
+
237
+ # Dev (run directly)
238
+ NEXTORA_API_URL=http://localhost:8080/api \
239
+ NEXTORA_API_KEY=nxt_xxx \
240
+ npm run dev
241
+
242
+ # Build for production
243
+ npm run build
244
+ node dist/index.js
245
+
246
+ # Docker
247
+ docker build -t nextora-mcp .
248
+ docker run -i \
249
+ -e NEXTORA_API_URL="https://your-domain/api" \
250
+ -e NEXTORA_API_KEY="nxt_xxx" \
251
+ nextora-mcp
252
+ ```
253
+
254
+ > `-i` (interactive / stdin open) จำเป็นสำหรับ stdio transport
255
+
256
+ ---
257
+
258
+ ## 8. Pattern สำคัญ
259
+
260
+ | Pattern | เหตุผล |
261
+ |---|---|
262
+ | `stderr` สำหรับ log | stdout ถูก JSON-RPC ใช้งานอยู่ — ห้าม `console.log` |
263
+ | Zod `.describe()` ครบทุก field | AI อ่านเพื่อเข้าใจว่าต้องส่งอะไร |
264
+ | Return plain text | AI อ่านแล้วตอบ user ได้ทันที ไม่ต้อง parse JSON |
265
+ | `autoRecord: true` default | UX ดีกว่า — user บอก "ส่งใบเสร็จ" ก็บันทึกเลย |
266
+ | Partial category match | ลด friction — user พิมพ์ "กาแฟ" แทน ID |