@promptpartner/bexio-mcp-server 2.0.1 → 2.0.3
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 +52 -39
- package/dist/index.js +7 -3
- package/dist/package.json +3 -0
- package/dist/ui/ui/contact-card/contact-card.html +187 -0
- package/dist/ui/ui/dashboard/dashboard.html +186 -0
- package/dist/ui/ui/invoice-preview/invoice-preview.html +196 -0
- package/dist/ui-resources.js +2 -2
- package/dist/vite.config.js +1 -1
- package/package.json +12 -9
- package/dist/ui/contact-card/mcp-app.d.ts +0 -1
- package/dist/ui/contact-card/mcp-app.js +0 -108
- package/dist/ui/dashboard/mcp-app.d.ts +0 -1
- package/dist/ui/dashboard/mcp-app.js +0 -81
- package/dist/ui/invoice-preview/mcp-app.d.ts +0 -1
- package/dist/ui/invoice-preview/mcp-app.js +0 -96
package/README.md
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
# @promptpartner/bexio-mcp-server
|
|
2
2
|
|
|
3
|
-
Complete Swiss accounting integration for [Bexio](https://www.bexio.com/)
|
|
3
|
+
Complete Swiss accounting integration for [Bexio](https://www.bexio.com/) via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/). Works with **Claude Desktop**, **n8n**, and any MCP-compatible client.
|
|
4
|
+
|
|
5
|
+
Manage invoices, contacts, projects, time tracking, and 200+ more tools through AI conversation or workflow automation.
|
|
6
|
+
|
|
7
|
+
## Compatibility
|
|
8
|
+
|
|
9
|
+
| Client | Transport | Status |
|
|
10
|
+
|--------|-----------|--------|
|
|
11
|
+
| [Claude Desktop](https://claude.ai/download) | stdio | ✅ Fully supported |
|
|
12
|
+
| [n8n](https://n8n.io/) | HTTP | ✅ Fully supported |
|
|
13
|
+
| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | stdio | ✅ Fully supported |
|
|
14
|
+
| Other MCP clients | stdio/HTTP | ✅ Should work |
|
|
4
15
|
|
|
5
16
|
## Quick Start
|
|
6
17
|
|
|
7
|
-
###
|
|
18
|
+
### For Claude Desktop
|
|
19
|
+
|
|
20
|
+
**Option A: MCPB Bundle (Easiest)**
|
|
8
21
|
|
|
9
22
|
1. Download `bexio-mcp-server.mcpb` from [Releases](https://github.com/promptpartner/bexio-mcp-server/releases)
|
|
10
|
-
2. Double-click to install
|
|
23
|
+
2. Double-click to install
|
|
11
24
|
3. Enter your Bexio API token when prompted
|
|
12
25
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npx @promptpartner/bexio-mcp-server
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Add to Claude Desktop config:
|
|
26
|
+
**Option B: npm**
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
28
|
+
Add to `claude_desktop_config.json`:
|
|
23
29
|
|
|
24
30
|
```json
|
|
25
31
|
{
|
|
@@ -35,29 +41,33 @@ Add to Claude Desktop config:
|
|
|
35
41
|
}
|
|
36
42
|
```
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
Config location:
|
|
45
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
46
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
47
|
+
|
|
48
|
+
### For n8n and Other HTTP Clients
|
|
49
|
+
|
|
50
|
+
Start the server in HTTP mode:
|
|
39
51
|
|
|
40
52
|
```bash
|
|
41
|
-
|
|
42
|
-
cd bexio-mcp-server/src
|
|
43
|
-
npm install
|
|
44
|
-
npm run build
|
|
53
|
+
BEXIO_API_TOKEN=your-token npx @promptpartner/bexio-mcp-server --mode http --port 8000
|
|
45
54
|
```
|
|
46
55
|
|
|
47
|
-
|
|
56
|
+
The server exposes MCP over HTTP at `http://localhost:8000`. Configure your MCP client to connect to this endpoint.
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
### For Other stdio Clients
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
BEXIO_API_TOKEN=your-token npx @promptpartner/bexio-mcp-server
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or build from source:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/promptpartner/bexio-mcp-server
|
|
68
|
+
cd bexio-mcp-server/src
|
|
69
|
+
npm install && npm run build
|
|
70
|
+
BEXIO_API_TOKEN=your-token node dist/index.js
|
|
61
71
|
```
|
|
62
72
|
|
|
63
73
|
## Getting Your Bexio API Token
|
|
@@ -81,7 +91,7 @@ This MCP server provides **221 tools** across all Bexio domains:
|
|
|
81
91
|
- Quotes with accept/decline workflows
|
|
82
92
|
- Orders with delivery management
|
|
83
93
|
- Incoming payments tracking
|
|
84
|
-
- Interactive invoice preview
|
|
94
|
+
- Interactive invoice preview (Claude Desktop)
|
|
85
95
|
|
|
86
96
|
### Banking & Payments
|
|
87
97
|
- Swiss QR-bill payment support (QR-IBAN)
|
|
@@ -115,14 +125,6 @@ This MCP server provides **221 tools** across all Bexio domains:
|
|
|
115
125
|
- Absence tracking
|
|
116
126
|
- Payroll documents
|
|
117
127
|
|
|
118
|
-
## HTTP Mode
|
|
119
|
-
|
|
120
|
-
For integration with n8n or other automation tools:
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
npx @promptpartner/bexio-mcp-server --mode http --port 8000
|
|
124
|
-
```
|
|
125
|
-
|
|
126
128
|
## Environment Variables
|
|
127
129
|
|
|
128
130
|
| Variable | Required | Default | Description |
|
|
@@ -130,6 +132,17 @@ npx @promptpartner/bexio-mcp-server --mode http --port 8000
|
|
|
130
132
|
| `BEXIO_API_TOKEN` | Yes | - | Your Bexio API token |
|
|
131
133
|
| `BEXIO_BASE_URL` | No | `https://api.bexio.com/2.0` | API endpoint URL |
|
|
132
134
|
|
|
135
|
+
## Command Line Options
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npx @promptpartner/bexio-mcp-server [options]
|
|
139
|
+
|
|
140
|
+
Options:
|
|
141
|
+
--mode <stdio|http> Transport mode (default: stdio)
|
|
142
|
+
--host <address> HTTP host (default: 0.0.0.0)
|
|
143
|
+
--port <number> HTTP port (default: 8000)
|
|
144
|
+
```
|
|
145
|
+
|
|
133
146
|
## Troubleshooting
|
|
134
147
|
|
|
135
148
|
### "Invalid API token" error
|
package/dist/index.js
CHANGED
|
@@ -12,13 +12,17 @@
|
|
|
12
12
|
* IMPORTANT: All logging goes to stderr via logger.ts.
|
|
13
13
|
* stdout is reserved for MCP JSON-RPC protocol messages (stdio mode only).
|
|
14
14
|
*/
|
|
15
|
-
import { config } from "dotenv";
|
|
16
15
|
import { BexioMcpServer } from "./server.js";
|
|
17
16
|
import { BexioClient } from "./bexio-client.js";
|
|
18
17
|
import { logger } from "./logger.js";
|
|
19
18
|
import { createHttpServer } from "./transports/http.js";
|
|
20
|
-
// Load environment variables from .env file
|
|
21
|
-
|
|
19
|
+
// Load environment variables from .env file (optional - for development)
|
|
20
|
+
// In MCPB bundles, env vars are already set by Claude Desktop
|
|
21
|
+
import("dotenv")
|
|
22
|
+
.then((dotenv) => dotenv.config())
|
|
23
|
+
.catch(() => {
|
|
24
|
+
// dotenv not available - that's fine for MCPB bundles
|
|
25
|
+
});
|
|
22
26
|
// Configuration from environment
|
|
23
27
|
const BEXIO_API_TOKEN = process.env["BEXIO_API_TOKEN"];
|
|
24
28
|
const BEXIO_BASE_URL = process.env["BEXIO_BASE_URL"] ?? "https://api.bexio.com/2.0";
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Contact Card</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
15
|
+
line-height: 1.5;
|
|
16
|
+
color: #1a1a1a;
|
|
17
|
+
background: #f9fafb;
|
|
18
|
+
padding: 1.5rem;
|
|
19
|
+
}
|
|
20
|
+
.card {
|
|
21
|
+
max-width: 400px;
|
|
22
|
+
margin: 0 auto;
|
|
23
|
+
background: #fff;
|
|
24
|
+
border-radius: 1rem;
|
|
25
|
+
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
}
|
|
28
|
+
.card-header {
|
|
29
|
+
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
|
30
|
+
color: white;
|
|
31
|
+
padding: 1.5rem;
|
|
32
|
+
text-align: center;
|
|
33
|
+
}
|
|
34
|
+
.avatar {
|
|
35
|
+
width: 80px;
|
|
36
|
+
height: 80px;
|
|
37
|
+
background: rgba(255, 255, 255, 0.2);
|
|
38
|
+
border-radius: 50%;
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: center;
|
|
42
|
+
margin: 0 auto 1rem;
|
|
43
|
+
font-size: 2rem;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
}
|
|
46
|
+
.card-header h1 {
|
|
47
|
+
font-size: 1.25rem;
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
margin-bottom: 0.25rem;
|
|
50
|
+
}
|
|
51
|
+
.card-header .company {
|
|
52
|
+
font-size: 0.875rem;
|
|
53
|
+
opacity: 0.9;
|
|
54
|
+
}
|
|
55
|
+
.card-body {
|
|
56
|
+
padding: 1.25rem;
|
|
57
|
+
}
|
|
58
|
+
.info-section {
|
|
59
|
+
margin-bottom: 1.25rem;
|
|
60
|
+
}
|
|
61
|
+
.info-section:last-child {
|
|
62
|
+
margin-bottom: 0;
|
|
63
|
+
}
|
|
64
|
+
.info-label {
|
|
65
|
+
font-size: 0.7rem;
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
text-transform: uppercase;
|
|
68
|
+
color: #6b7280;
|
|
69
|
+
margin-bottom: 0.25rem;
|
|
70
|
+
}
|
|
71
|
+
.info-row {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
padding: 0.5rem 0;
|
|
75
|
+
border-bottom: 1px solid #f3f4f6;
|
|
76
|
+
}
|
|
77
|
+
.info-row:last-child {
|
|
78
|
+
border-bottom: none;
|
|
79
|
+
}
|
|
80
|
+
.info-icon {
|
|
81
|
+
width: 20px;
|
|
82
|
+
height: 20px;
|
|
83
|
+
margin-right: 0.75rem;
|
|
84
|
+
color: #6b7280;
|
|
85
|
+
}
|
|
86
|
+
.info-value {
|
|
87
|
+
color: #374151;
|
|
88
|
+
}
|
|
89
|
+
.info-value a {
|
|
90
|
+
color: #3b82f6;
|
|
91
|
+
text-decoration: none;
|
|
92
|
+
}
|
|
93
|
+
.info-value a:hover {
|
|
94
|
+
text-decoration: underline;
|
|
95
|
+
}
|
|
96
|
+
.address-block {
|
|
97
|
+
color: #374151;
|
|
98
|
+
font-size: 0.95rem;
|
|
99
|
+
}
|
|
100
|
+
.loading {
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
justify-content: center;
|
|
104
|
+
min-height: 200px;
|
|
105
|
+
color: #6b7280;
|
|
106
|
+
background: #fff;
|
|
107
|
+
}
|
|
108
|
+
.type-badge {
|
|
109
|
+
display: inline-block;
|
|
110
|
+
padding: 0.125rem 0.5rem;
|
|
111
|
+
border-radius: 9999px;
|
|
112
|
+
font-size: 0.7rem;
|
|
113
|
+
font-weight: 600;
|
|
114
|
+
text-transform: uppercase;
|
|
115
|
+
margin-top: 0.5rem;
|
|
116
|
+
}
|
|
117
|
+
.type-company { background: rgba(255, 255, 255, 0.2); }
|
|
118
|
+
.type-person { background: rgba(255, 255, 255, 0.2); }
|
|
119
|
+
</style>
|
|
120
|
+
<script type="module" crossorigin>import{g as f}from"./app-CW49uSHM.js";const o=document.getElementById("app"),t=new f({name:"Contact Card",version:"1.0.0"});t.ontoolresult=e=>{var n,a;const s=(a=(n=e.content)==null?void 0:n.find(i=>i.type==="text"))==null?void 0:a.text;if(s)try{const i=JSON.parse(s);m(i)}catch{o.innerHTML='<div class="loading">Error parsing contact data</div>'}};t.connect();function c(e,s){return[e,s].filter(Boolean).map(a=>a.charAt(0).toUpperCase()).join("").slice(0,2)||"?"}function h(e){return e?{1:"Switzerland",2:"Germany",3:"Austria",4:"France",5:"Italy",6:"Liechtenstein"}[e]||`Country #${e}`:""}function m(e){const s=[e.name_1,e.name_2].filter(Boolean).join(" "),n=c(e.name_1,e.name_2),a=e.contact_type_id===1,i=a?"Company":"Person",v=a?"type-company":"type-person",l=[e.address,[e.postcode,e.city].filter(Boolean).join(" "),h(e.country_id)].filter(Boolean),r=e.mail||e.phone_fixed||e.phone_mobile||e.fax,d=l.length>0;o.className="card",o.innerHTML=`
|
|
121
|
+
<div class="card-header">
|
|
122
|
+
<div class="avatar">${n}</div>
|
|
123
|
+
<h1>${s}</h1>
|
|
124
|
+
${e.url?`<div class="company">${e.url}</div>`:""}
|
|
125
|
+
<span class="type-badge ${v}">${i}</span>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="card-body">
|
|
128
|
+
${r?`
|
|
129
|
+
<div class="info-section">
|
|
130
|
+
<div class="info-label">Contact Information</div>
|
|
131
|
+
${e.mail?`
|
|
132
|
+
<div class="info-row">
|
|
133
|
+
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
134
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
135
|
+
</svg>
|
|
136
|
+
<span class="info-value"><a href="mailto:${e.mail}">${e.mail}</a></span>
|
|
137
|
+
</div>
|
|
138
|
+
`:""}
|
|
139
|
+
${e.phone_fixed?`
|
|
140
|
+
<div class="info-row">
|
|
141
|
+
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
142
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
|
|
143
|
+
</svg>
|
|
144
|
+
<span class="info-value"><a href="tel:${e.phone_fixed}">${e.phone_fixed}</a></span>
|
|
145
|
+
</div>
|
|
146
|
+
`:""}
|
|
147
|
+
${e.phone_mobile?`
|
|
148
|
+
<div class="info-row">
|
|
149
|
+
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
150
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"/>
|
|
151
|
+
</svg>
|
|
152
|
+
<span class="info-value"><a href="tel:${e.phone_mobile}">${e.phone_mobile}</a></span>
|
|
153
|
+
</div>
|
|
154
|
+
`:""}
|
|
155
|
+
${e.fax?`
|
|
156
|
+
<div class="info-row">
|
|
157
|
+
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
158
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
|
|
159
|
+
</svg>
|
|
160
|
+
<span class="info-value">${e.fax}</span>
|
|
161
|
+
</div>
|
|
162
|
+
`:""}
|
|
163
|
+
</div>
|
|
164
|
+
`:""}
|
|
165
|
+
|
|
166
|
+
${d?`
|
|
167
|
+
<div class="info-section">
|
|
168
|
+
<div class="info-label">Address</div>
|
|
169
|
+
<div class="address-block">
|
|
170
|
+
${l.map(p=>`<div>${p}</div>`).join("")}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
`:""}
|
|
174
|
+
|
|
175
|
+
${!r&&!d?`
|
|
176
|
+
<div class="info-section" style="text-align: center; color: #6b7280;">
|
|
177
|
+
No contact details available
|
|
178
|
+
</div>
|
|
179
|
+
`:""}
|
|
180
|
+
</div>
|
|
181
|
+
`}</script>
|
|
182
|
+
<link rel="modulepreload" crossorigin href="../../app-CW49uSHM.js">
|
|
183
|
+
</head>
|
|
184
|
+
<body>
|
|
185
|
+
<div id="app" class="card loading">Loading contact...</div>
|
|
186
|
+
</body>
|
|
187
|
+
</html>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Bexio Dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
15
|
+
line-height: 1.5;
|
|
16
|
+
color: #1a1a1a;
|
|
17
|
+
background: #f3f4f6;
|
|
18
|
+
padding: 1.5rem;
|
|
19
|
+
}
|
|
20
|
+
.dashboard {
|
|
21
|
+
max-width: 900px;
|
|
22
|
+
margin: 0 auto;
|
|
23
|
+
}
|
|
24
|
+
.header {
|
|
25
|
+
margin-bottom: 1.5rem;
|
|
26
|
+
}
|
|
27
|
+
.header h1 {
|
|
28
|
+
font-size: 1.5rem;
|
|
29
|
+
font-weight: 600;
|
|
30
|
+
color: #111827;
|
|
31
|
+
}
|
|
32
|
+
.header .subtitle {
|
|
33
|
+
color: #6b7280;
|
|
34
|
+
font-size: 0.875rem;
|
|
35
|
+
}
|
|
36
|
+
.grid {
|
|
37
|
+
display: grid;
|
|
38
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
39
|
+
gap: 1rem;
|
|
40
|
+
}
|
|
41
|
+
.card {
|
|
42
|
+
background: #fff;
|
|
43
|
+
border-radius: 0.75rem;
|
|
44
|
+
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
45
|
+
padding: 1.25rem;
|
|
46
|
+
}
|
|
47
|
+
.card-header {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
margin-bottom: 0.75rem;
|
|
51
|
+
}
|
|
52
|
+
.card-icon {
|
|
53
|
+
width: 40px;
|
|
54
|
+
height: 40px;
|
|
55
|
+
border-radius: 0.5rem;
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
margin-right: 0.75rem;
|
|
60
|
+
}
|
|
61
|
+
.card-icon svg {
|
|
62
|
+
width: 24px;
|
|
63
|
+
height: 24px;
|
|
64
|
+
}
|
|
65
|
+
.card-icon.blue { background: #dbeafe; color: #2563eb; }
|
|
66
|
+
.card-icon.yellow { background: #fef3c7; color: #d97706; }
|
|
67
|
+
.card-icon.green { background: #d1fae5; color: #059669; }
|
|
68
|
+
.card-title {
|
|
69
|
+
font-size: 0.875rem;
|
|
70
|
+
font-weight: 500;
|
|
71
|
+
color: #6b7280;
|
|
72
|
+
}
|
|
73
|
+
.card-value {
|
|
74
|
+
font-size: 2rem;
|
|
75
|
+
font-weight: 700;
|
|
76
|
+
color: #111827;
|
|
77
|
+
margin-bottom: 0.25rem;
|
|
78
|
+
}
|
|
79
|
+
.card-subtitle {
|
|
80
|
+
font-size: 0.875rem;
|
|
81
|
+
color: #6b7280;
|
|
82
|
+
}
|
|
83
|
+
.contacts-list {
|
|
84
|
+
margin-top: 0.5rem;
|
|
85
|
+
}
|
|
86
|
+
.contact-item {
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
padding: 0.5rem 0;
|
|
90
|
+
border-bottom: 1px solid #f3f4f6;
|
|
91
|
+
}
|
|
92
|
+
.contact-item:last-child {
|
|
93
|
+
border-bottom: none;
|
|
94
|
+
}
|
|
95
|
+
.contact-avatar {
|
|
96
|
+
width: 32px;
|
|
97
|
+
height: 32px;
|
|
98
|
+
border-radius: 50%;
|
|
99
|
+
background: #e5e7eb;
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
justify-content: center;
|
|
103
|
+
font-size: 0.75rem;
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
color: #6b7280;
|
|
106
|
+
margin-right: 0.75rem;
|
|
107
|
+
}
|
|
108
|
+
.contact-name {
|
|
109
|
+
font-size: 0.875rem;
|
|
110
|
+
color: #374151;
|
|
111
|
+
}
|
|
112
|
+
.loading {
|
|
113
|
+
display: flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
min-height: 200px;
|
|
117
|
+
color: #6b7280;
|
|
118
|
+
}
|
|
119
|
+
.empty-state {
|
|
120
|
+
text-align: center;
|
|
121
|
+
color: #6b7280;
|
|
122
|
+
padding: 1rem;
|
|
123
|
+
font-size: 0.875rem;
|
|
124
|
+
}
|
|
125
|
+
</style>
|
|
126
|
+
<script type="module" crossorigin>import{g as r}from"./app-CW49uSHM.js";const t=document.getElementById("app"),o=new r({name:"Bexio Dashboard",version:"1.0.0"});o.ontoolresult=e=>{var n,s;const a=(s=(n=e.content)==null?void 0:n.find(i=>i.type==="text"))==null?void 0:s.text;if(a)try{const i=JSON.parse(a);l(i)}catch{t.innerHTML='<div class="loading">Error parsing dashboard data</div>'}};o.connect();function c(e,a){return`${a} ${e.toLocaleString("de-CH",{minimumFractionDigits:2,maximumFractionDigits:2})}`}function d(e,a){return[e,a].filter(Boolean).map(s=>s.charAt(0).toUpperCase()).join("").slice(0,2)||"?"}function l(e){t.className="dashboard",t.innerHTML=`
|
|
127
|
+
<div class="header">
|
|
128
|
+
<h1>Bexio Dashboard</h1>
|
|
129
|
+
<p class="subtitle">Overview of your invoices and recent contacts</p>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div class="grid">
|
|
133
|
+
<div class="card">
|
|
134
|
+
<div class="card-header">
|
|
135
|
+
<div class="card-icon blue">
|
|
136
|
+
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
137
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
138
|
+
</svg>
|
|
139
|
+
</div>
|
|
140
|
+
<span class="card-title">Open Invoices</span>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="card-value">${e.open_invoices_count}</div>
|
|
143
|
+
<div class="card-subtitle">${c(e.open_invoices_total,e.currency)} total</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="card">
|
|
147
|
+
<div class="card-header">
|
|
148
|
+
<div class="card-icon yellow">
|
|
149
|
+
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
150
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
151
|
+
</svg>
|
|
152
|
+
</div>
|
|
153
|
+
<span class="card-title">Overdue</span>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="card-value">${e.overdue_count}</div>
|
|
156
|
+
<div class="card-subtitle">${c(e.overdue_total,e.currency)} outstanding</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div class="card">
|
|
160
|
+
<div class="card-header">
|
|
161
|
+
<div class="card-icon green">
|
|
162
|
+
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
163
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
|
164
|
+
</svg>
|
|
165
|
+
</div>
|
|
166
|
+
<span class="card-title">Recent Contacts</span>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="contacts-list">
|
|
169
|
+
${e.recent_contacts.length>0?e.recent_contacts.slice(0,5).map(a=>`
|
|
170
|
+
<div class="contact-item">
|
|
171
|
+
<div class="contact-avatar">${d(a.name_1,a.name_2)}</div>
|
|
172
|
+
<span class="contact-name">${[a.name_1,a.name_2].filter(Boolean).join(" ")}</span>
|
|
173
|
+
</div>
|
|
174
|
+
`).join(""):`
|
|
175
|
+
<div class="empty-state">No recent contacts</div>
|
|
176
|
+
`}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
`}</script>
|
|
181
|
+
<link rel="modulepreload" crossorigin href="../../app-CW49uSHM.js">
|
|
182
|
+
</head>
|
|
183
|
+
<body>
|
|
184
|
+
<div id="app" class="loading">Loading dashboard...</div>
|
|
185
|
+
</body>
|
|
186
|
+
</html>
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Invoice Preview</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
15
|
+
line-height: 1.5;
|
|
16
|
+
color: #1a1a1a;
|
|
17
|
+
background: #fff;
|
|
18
|
+
padding: 1.5rem;
|
|
19
|
+
}
|
|
20
|
+
.invoice {
|
|
21
|
+
max-width: 800px;
|
|
22
|
+
margin: 0 auto;
|
|
23
|
+
}
|
|
24
|
+
.header {
|
|
25
|
+
display: flex;
|
|
26
|
+
justify-content: space-between;
|
|
27
|
+
align-items: flex-start;
|
|
28
|
+
margin-bottom: 2rem;
|
|
29
|
+
padding-bottom: 1.5rem;
|
|
30
|
+
border-bottom: 2px solid #e5e7eb;
|
|
31
|
+
}
|
|
32
|
+
.header-left h1 {
|
|
33
|
+
font-size: 1.75rem;
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
color: #111827;
|
|
36
|
+
margin-bottom: 0.25rem;
|
|
37
|
+
}
|
|
38
|
+
.header-left .title {
|
|
39
|
+
color: #6b7280;
|
|
40
|
+
font-size: 0.95rem;
|
|
41
|
+
}
|
|
42
|
+
.header-right {
|
|
43
|
+
text-align: right;
|
|
44
|
+
}
|
|
45
|
+
.header-right .dates {
|
|
46
|
+
font-size: 0.875rem;
|
|
47
|
+
color: #6b7280;
|
|
48
|
+
}
|
|
49
|
+
.status-badge {
|
|
50
|
+
display: inline-block;
|
|
51
|
+
padding: 0.25rem 0.75rem;
|
|
52
|
+
border-radius: 9999px;
|
|
53
|
+
font-size: 0.75rem;
|
|
54
|
+
font-weight: 600;
|
|
55
|
+
text-transform: uppercase;
|
|
56
|
+
margin-top: 0.5rem;
|
|
57
|
+
}
|
|
58
|
+
.status-draft { background: #e5e7eb; color: #374151; }
|
|
59
|
+
.status-pending { background: #fef3c7; color: #92400e; }
|
|
60
|
+
.status-sent { background: #dbeafe; color: #1e40af; }
|
|
61
|
+
.status-paid { background: #d1fae5; color: #065f46; }
|
|
62
|
+
.status-overdue { background: #fee2e2; color: #991b1b; }
|
|
63
|
+
.status-cancelled { background: #f3f4f6; color: #6b7280; }
|
|
64
|
+
.contact-section {
|
|
65
|
+
background: #f9fafb;
|
|
66
|
+
padding: 1rem 1.25rem;
|
|
67
|
+
border-radius: 0.5rem;
|
|
68
|
+
margin-bottom: 1.5rem;
|
|
69
|
+
}
|
|
70
|
+
.contact-section h2 {
|
|
71
|
+
font-size: 0.75rem;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
text-transform: uppercase;
|
|
74
|
+
color: #6b7280;
|
|
75
|
+
margin-bottom: 0.5rem;
|
|
76
|
+
}
|
|
77
|
+
.contact-section .name {
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
color: #111827;
|
|
80
|
+
}
|
|
81
|
+
.contact-section .email {
|
|
82
|
+
color: #6b7280;
|
|
83
|
+
font-size: 0.875rem;
|
|
84
|
+
}
|
|
85
|
+
.line-items {
|
|
86
|
+
width: 100%;
|
|
87
|
+
border-collapse: collapse;
|
|
88
|
+
margin-bottom: 1.5rem;
|
|
89
|
+
}
|
|
90
|
+
.line-items th {
|
|
91
|
+
text-align: left;
|
|
92
|
+
padding: 0.75rem 1rem;
|
|
93
|
+
font-size: 0.75rem;
|
|
94
|
+
font-weight: 600;
|
|
95
|
+
text-transform: uppercase;
|
|
96
|
+
color: #6b7280;
|
|
97
|
+
background: #f9fafb;
|
|
98
|
+
border-bottom: 1px solid #e5e7eb;
|
|
99
|
+
}
|
|
100
|
+
.line-items th:last-child,
|
|
101
|
+
.line-items td:last-child {
|
|
102
|
+
text-align: right;
|
|
103
|
+
}
|
|
104
|
+
.line-items td {
|
|
105
|
+
padding: 0.75rem 1rem;
|
|
106
|
+
border-bottom: 1px solid #f3f4f6;
|
|
107
|
+
}
|
|
108
|
+
.total-section {
|
|
109
|
+
display: flex;
|
|
110
|
+
justify-content: flex-end;
|
|
111
|
+
padding-top: 1rem;
|
|
112
|
+
border-top: 2px solid #e5e7eb;
|
|
113
|
+
}
|
|
114
|
+
.total-row {
|
|
115
|
+
display: flex;
|
|
116
|
+
justify-content: space-between;
|
|
117
|
+
min-width: 200px;
|
|
118
|
+
}
|
|
119
|
+
.total-label {
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
color: #374151;
|
|
122
|
+
}
|
|
123
|
+
.total-amount {
|
|
124
|
+
font-size: 1.25rem;
|
|
125
|
+
font-weight: 700;
|
|
126
|
+
color: #111827;
|
|
127
|
+
}
|
|
128
|
+
.loading {
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
justify-content: center;
|
|
132
|
+
min-height: 200px;
|
|
133
|
+
color: #6b7280;
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
136
|
+
<script type="module" crossorigin>import{g as o}from"./app-CW49uSHM.js";const r=document.getElementById("app"),i=new o({name:"Invoice Preview",version:"1.0.0"});i.ontoolresult=t=>{var e,n;const a=(n=(e=t.content)==null?void 0:e.find(s=>s.type==="text"))==null?void 0:n.text;if(a)try{const s=JSON.parse(a);m(s)}catch{r.innerHTML='<div class="loading">Error parsing invoice data</div>'}};i.connect();function u(t){switch(t){case 7:return{label:"Draft",className:"status-draft"};case 8:return{label:"Pending",className:"status-pending"};case 9:return{label:"Sent",className:"status-sent"};case 16:return{label:"Paid",className:"status-paid"};case 17:return{label:"Overdue",className:"status-overdue"};case 19:return{label:"Cancelled",className:"status-cancelled"};default:return{label:`Status ${t}`,className:"status-draft"}}}function c(t,a){const e=typeof t=="string"?parseFloat(t):t;return`${a===1?"CHF":a===2?"EUR":"USD"} ${e.toFixed(2)}`}function d(t){return t?new Date(t).toLocaleDateString("de-CH",{day:"2-digit",month:"2-digit",year:"numeric"}):""}function m(t){var n,s;const a=u(t.kb_item_status_id),e=t.positions||[];r.className="invoice",r.innerHTML=`
|
|
137
|
+
<div class="header">
|
|
138
|
+
<div class="header-left">
|
|
139
|
+
<h1>Invoice ${t.document_nr}</h1>
|
|
140
|
+
<p class="title">${t.title||""}</p>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="header-right">
|
|
143
|
+
<div class="dates">
|
|
144
|
+
<div>Issue: ${d(t.is_valid_from)}</div>
|
|
145
|
+
<div>Due: ${d(t.is_valid_to)}</div>
|
|
146
|
+
</div>
|
|
147
|
+
<span class="status-badge ${a.className}">${a.label}</span>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="contact-section">
|
|
152
|
+
<h2>Bill To</h2>
|
|
153
|
+
<div class="name">${((n=t.contact_address)==null?void 0:n.split(`
|
|
154
|
+
`)[0])||`Contact #${t.contact_id}`}</div>
|
|
155
|
+
<div class="email">${((s=t.contact_address)==null?void 0:s.split(`
|
|
156
|
+
`).slice(1).join(", "))||""}</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<table class="line-items">
|
|
160
|
+
<thead>
|
|
161
|
+
<tr>
|
|
162
|
+
<th>Description</th>
|
|
163
|
+
<th>Qty</th>
|
|
164
|
+
<th>Price</th>
|
|
165
|
+
<th>Amount</th>
|
|
166
|
+
</tr>
|
|
167
|
+
</thead>
|
|
168
|
+
<tbody>
|
|
169
|
+
${e.length>0?e.map(l=>`
|
|
170
|
+
<tr>
|
|
171
|
+
<td>${l.text}</td>
|
|
172
|
+
<td>${l.amount}</td>
|
|
173
|
+
<td>${c(l.unit_price,t.currency_id)}</td>
|
|
174
|
+
<td>${c(l.amount*l.unit_price*(1-(l.discount_in_percent||0)/100),t.currency_id)}</td>
|
|
175
|
+
</tr>
|
|
176
|
+
`).join(""):`
|
|
177
|
+
<tr>
|
|
178
|
+
<td colspan="4" style="text-align: center; color: #6b7280;">No line items available</td>
|
|
179
|
+
</tr>
|
|
180
|
+
`}
|
|
181
|
+
</tbody>
|
|
182
|
+
</table>
|
|
183
|
+
|
|
184
|
+
<div class="total-section">
|
|
185
|
+
<div class="total-row">
|
|
186
|
+
<span class="total-label">Total:</span>
|
|
187
|
+
<span class="total-amount">${c(t.total_gross,t.currency_id)}</span>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
`}</script>
|
|
191
|
+
<link rel="modulepreload" crossorigin href="../../app-CW49uSHM.js">
|
|
192
|
+
</head>
|
|
193
|
+
<body>
|
|
194
|
+
<div id="app" class="loading">Loading invoice...</div>
|
|
195
|
+
</body>
|
|
196
|
+
</html>
|
package/dist/ui-resources.js
CHANGED
|
@@ -22,8 +22,8 @@ const DASHBOARD_URI = "ui://bexio/dashboard.html";
|
|
|
22
22
|
* @param client - BexioClient for API calls
|
|
23
23
|
*/
|
|
24
24
|
export function registerUIResources(server, client) {
|
|
25
|
-
// Path to built UI files (vite outputs to dist/ui/ui/<name>/<name>.html)
|
|
26
|
-
const uiBasePath = path.join(import.meta.dirname, "
|
|
25
|
+
// Path to built UI files (vite outputs to src/dist/ui/ui/<name>/<name>.html)
|
|
26
|
+
const uiBasePath = path.join(import.meta.dirname, "ui/ui");
|
|
27
27
|
// ===== INVOICE PREVIEW =====
|
|
28
28
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
29
|
registerAppTool(server, "preview_invoice", {
|
package/dist/vite.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptpartner/bexio-mcp-server",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "Model Context Protocol server for Bexio API integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"LICENSE"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"build": "tsc && npm run build:ui",
|
|
16
|
+
"build": "tsc && npm run build:ui && npm run postbuild",
|
|
17
|
+
"postbuild": "node -e \"require('fs').writeFileSync('dist/package.json', JSON.stringify({type:'module'}, null, 2)+'\\n'); console.log('Created dist/package.json')\"",
|
|
17
18
|
"build:ui": "node -e \"const fs=require('fs'); if(fs.existsSync('ui')){const{execSync}=require('child_process');execSync('npx vite build',{stdio:'inherit'})}else{console.log('Skipping UI build - ui/ not found')}\"",
|
|
18
19
|
"dev": "tsx watch src/index.ts",
|
|
19
20
|
"dev:ui": "vite build --watch",
|
|
@@ -22,7 +23,8 @@
|
|
|
22
23
|
"type-check": "tsc --noEmit",
|
|
23
24
|
"clean": "node -e \"const fs=require('fs'); if(fs.existsSync('dist')){fs.rmSync('dist',{recursive:true}); console.log('Cleaned dist/')}\"",
|
|
24
25
|
"prebuild": "npm run clean",
|
|
25
|
-
"
|
|
26
|
+
"bundle:mcpb": "node scripts/bundle-mcpb.js",
|
|
27
|
+
"pack:mcpb": "npm run build && npm run bundle:mcpb && npm run copy:bundle && cd .. && mcpb pack",
|
|
26
28
|
"copy:bundle": "node -e \"const fs=require('fs'); const path=require('path'); function copyDir(src,dest){fs.mkdirSync(dest,{recursive:true});fs.readdirSync(src).forEach(f=>{const srcPath=path.join(src,f);const destPath=path.join(dest,f);fs.statSync(srcPath).isDirectory()?copyDir(srcPath,destPath):fs.copyFileSync(srcPath,destPath)})} const rootDist=path.join(__dirname,'..','dist'); if(fs.existsSync(rootDist))fs.rmSync(rootDist,{recursive:true}); copyDir('dist',rootDist); console.log('Copied dist/ to root for MCPB bundling')\"",
|
|
27
29
|
"validate:mcpb": "cd .. && mcpb validate manifest.json",
|
|
28
30
|
"prepublishOnly": "npm run build"
|
|
@@ -52,22 +54,23 @@
|
|
|
52
54
|
],
|
|
53
55
|
"license": "MIT",
|
|
54
56
|
"dependencies": {
|
|
55
|
-
"@
|
|
57
|
+
"@fastify/cors": "^9.0.0",
|
|
56
58
|
"@modelcontextprotocol/ext-apps": "^1.0.1",
|
|
59
|
+
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
57
60
|
"axios": "^1.7.0",
|
|
58
|
-
"zod": "3.25.76",
|
|
59
61
|
"dotenv": "^16.3.0",
|
|
60
62
|
"fastify": "^4.28.0",
|
|
61
|
-
"
|
|
63
|
+
"zod": "3.25.76"
|
|
62
64
|
},
|
|
63
65
|
"devDependencies": {
|
|
64
66
|
"@types/node": "^20.8.0",
|
|
65
|
-
"
|
|
67
|
+
"concurrently": "^8.0.0",
|
|
68
|
+
"esbuild": "^0.27.2",
|
|
66
69
|
"tsx": "^4.0.0",
|
|
67
|
-
"
|
|
70
|
+
"typescript": "^5.5.0",
|
|
68
71
|
"vite": "^6.0.0",
|
|
69
72
|
"vite-plugin-singlefile": "^2.3.0",
|
|
70
|
-
"
|
|
73
|
+
"vitest": "^2.0.0"
|
|
71
74
|
},
|
|
72
75
|
"engines": {
|
|
73
76
|
"node": ">=18.0.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { App } from "@modelcontextprotocol/ext-apps";
|
|
2
|
-
const appEl = document.getElementById("app");
|
|
3
|
-
const app = new App({ name: "Contact Card", version: "1.0.0" });
|
|
4
|
-
app.ontoolresult = (result) => {
|
|
5
|
-
const text = result.content?.find((c) => c.type === "text")?.text;
|
|
6
|
-
if (text) {
|
|
7
|
-
try {
|
|
8
|
-
const contact = JSON.parse(text);
|
|
9
|
-
renderContact(contact);
|
|
10
|
-
}
|
|
11
|
-
catch (e) {
|
|
12
|
-
appEl.innerHTML = `<div class="loading">Error parsing contact data</div>`;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
app.connect();
|
|
17
|
-
function getInitials(name1, name2) {
|
|
18
|
-
const parts = [name1, name2].filter(Boolean);
|
|
19
|
-
return parts.map(p => p.charAt(0).toUpperCase()).join("").slice(0, 2) || "?";
|
|
20
|
-
}
|
|
21
|
-
function getCountryName(countryId) {
|
|
22
|
-
const countries = {
|
|
23
|
-
1: "Switzerland",
|
|
24
|
-
2: "Germany",
|
|
25
|
-
3: "Austria",
|
|
26
|
-
4: "France",
|
|
27
|
-
5: "Italy",
|
|
28
|
-
6: "Liechtenstein",
|
|
29
|
-
};
|
|
30
|
-
return countryId ? countries[countryId] || `Country #${countryId}` : "";
|
|
31
|
-
}
|
|
32
|
-
function renderContact(contact) {
|
|
33
|
-
const fullName = [contact.name_1, contact.name_2].filter(Boolean).join(" ");
|
|
34
|
-
const initials = getInitials(contact.name_1, contact.name_2);
|
|
35
|
-
const isCompany = contact.contact_type_id === 1;
|
|
36
|
-
const typeBadge = isCompany ? "Company" : "Person";
|
|
37
|
-
const typeClass = isCompany ? "type-company" : "type-person";
|
|
38
|
-
const addressParts = [
|
|
39
|
-
contact.address,
|
|
40
|
-
[contact.postcode, contact.city].filter(Boolean).join(" "),
|
|
41
|
-
getCountryName(contact.country_id),
|
|
42
|
-
].filter(Boolean);
|
|
43
|
-
const hasContactInfo = contact.mail || contact.phone_fixed || contact.phone_mobile || contact.fax;
|
|
44
|
-
const hasAddress = addressParts.length > 0;
|
|
45
|
-
appEl.className = "card";
|
|
46
|
-
appEl.innerHTML = `
|
|
47
|
-
<div class="card-header">
|
|
48
|
-
<div class="avatar">${initials}</div>
|
|
49
|
-
<h1>${fullName}</h1>
|
|
50
|
-
${contact.url ? `<div class="company">${contact.url}</div>` : ""}
|
|
51
|
-
<span class="type-badge ${typeClass}">${typeBadge}</span>
|
|
52
|
-
</div>
|
|
53
|
-
<div class="card-body">
|
|
54
|
-
${hasContactInfo ? `
|
|
55
|
-
<div class="info-section">
|
|
56
|
-
<div class="info-label">Contact Information</div>
|
|
57
|
-
${contact.mail ? `
|
|
58
|
-
<div class="info-row">
|
|
59
|
-
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
60
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
61
|
-
</svg>
|
|
62
|
-
<span class="info-value"><a href="mailto:${contact.mail}">${contact.mail}</a></span>
|
|
63
|
-
</div>
|
|
64
|
-
` : ""}
|
|
65
|
-
${contact.phone_fixed ? `
|
|
66
|
-
<div class="info-row">
|
|
67
|
-
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
68
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
|
|
69
|
-
</svg>
|
|
70
|
-
<span class="info-value"><a href="tel:${contact.phone_fixed}">${contact.phone_fixed}</a></span>
|
|
71
|
-
</div>
|
|
72
|
-
` : ""}
|
|
73
|
-
${contact.phone_mobile ? `
|
|
74
|
-
<div class="info-row">
|
|
75
|
-
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
76
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"/>
|
|
77
|
-
</svg>
|
|
78
|
-
<span class="info-value"><a href="tel:${contact.phone_mobile}">${contact.phone_mobile}</a></span>
|
|
79
|
-
</div>
|
|
80
|
-
` : ""}
|
|
81
|
-
${contact.fax ? `
|
|
82
|
-
<div class="info-row">
|
|
83
|
-
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
84
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
|
|
85
|
-
</svg>
|
|
86
|
-
<span class="info-value">${contact.fax}</span>
|
|
87
|
-
</div>
|
|
88
|
-
` : ""}
|
|
89
|
-
</div>
|
|
90
|
-
` : ""}
|
|
91
|
-
|
|
92
|
-
${hasAddress ? `
|
|
93
|
-
<div class="info-section">
|
|
94
|
-
<div class="info-label">Address</div>
|
|
95
|
-
<div class="address-block">
|
|
96
|
-
${addressParts.map(p => `<div>${p}</div>`).join("")}
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
` : ""}
|
|
100
|
-
|
|
101
|
-
${!hasContactInfo && !hasAddress ? `
|
|
102
|
-
<div class="info-section" style="text-align: center; color: #6b7280;">
|
|
103
|
-
No contact details available
|
|
104
|
-
</div>
|
|
105
|
-
` : ""}
|
|
106
|
-
</div>
|
|
107
|
-
`;
|
|
108
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { App } from "@modelcontextprotocol/ext-apps";
|
|
2
|
-
const appEl = document.getElementById("app");
|
|
3
|
-
const app = new App({ name: "Bexio Dashboard", version: "1.0.0" });
|
|
4
|
-
app.ontoolresult = (result) => {
|
|
5
|
-
const text = result.content?.find((c) => c.type === "text")?.text;
|
|
6
|
-
if (text) {
|
|
7
|
-
try {
|
|
8
|
-
const data = JSON.parse(text);
|
|
9
|
-
renderDashboard(data);
|
|
10
|
-
}
|
|
11
|
-
catch (e) {
|
|
12
|
-
appEl.innerHTML = `<div class="loading">Error parsing dashboard data</div>`;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
app.connect();
|
|
17
|
-
function formatCurrency(amount, currency) {
|
|
18
|
-
return `${currency} ${amount.toLocaleString("de-CH", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
19
|
-
}
|
|
20
|
-
function getInitials(name1, name2) {
|
|
21
|
-
const parts = [name1, name2].filter(Boolean);
|
|
22
|
-
return parts.map(p => p.charAt(0).toUpperCase()).join("").slice(0, 2) || "?";
|
|
23
|
-
}
|
|
24
|
-
function renderDashboard(data) {
|
|
25
|
-
appEl.className = "dashboard";
|
|
26
|
-
appEl.innerHTML = `
|
|
27
|
-
<div class="header">
|
|
28
|
-
<h1>Bexio Dashboard</h1>
|
|
29
|
-
<p class="subtitle">Overview of your invoices and recent contacts</p>
|
|
30
|
-
</div>
|
|
31
|
-
|
|
32
|
-
<div class="grid">
|
|
33
|
-
<div class="card">
|
|
34
|
-
<div class="card-header">
|
|
35
|
-
<div class="card-icon blue">
|
|
36
|
-
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
37
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
38
|
-
</svg>
|
|
39
|
-
</div>
|
|
40
|
-
<span class="card-title">Open Invoices</span>
|
|
41
|
-
</div>
|
|
42
|
-
<div class="card-value">${data.open_invoices_count}</div>
|
|
43
|
-
<div class="card-subtitle">${formatCurrency(data.open_invoices_total, data.currency)} total</div>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<div class="card">
|
|
47
|
-
<div class="card-header">
|
|
48
|
-
<div class="card-icon yellow">
|
|
49
|
-
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
50
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
51
|
-
</svg>
|
|
52
|
-
</div>
|
|
53
|
-
<span class="card-title">Overdue</span>
|
|
54
|
-
</div>
|
|
55
|
-
<div class="card-value">${data.overdue_count}</div>
|
|
56
|
-
<div class="card-subtitle">${formatCurrency(data.overdue_total, data.currency)} outstanding</div>
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<div class="card">
|
|
60
|
-
<div class="card-header">
|
|
61
|
-
<div class="card-icon green">
|
|
62
|
-
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
63
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
|
64
|
-
</svg>
|
|
65
|
-
</div>
|
|
66
|
-
<span class="card-title">Recent Contacts</span>
|
|
67
|
-
</div>
|
|
68
|
-
<div class="contacts-list">
|
|
69
|
-
${data.recent_contacts.length > 0 ? data.recent_contacts.slice(0, 5).map(contact => `
|
|
70
|
-
<div class="contact-item">
|
|
71
|
-
<div class="contact-avatar">${getInitials(contact.name_1, contact.name_2)}</div>
|
|
72
|
-
<span class="contact-name">${[contact.name_1, contact.name_2].filter(Boolean).join(" ")}</span>
|
|
73
|
-
</div>
|
|
74
|
-
`).join("") : `
|
|
75
|
-
<div class="empty-state">No recent contacts</div>
|
|
76
|
-
`}
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
80
|
-
`;
|
|
81
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { App } from "@modelcontextprotocol/ext-apps";
|
|
2
|
-
const appEl = document.getElementById("app");
|
|
3
|
-
const app = new App({ name: "Invoice Preview", version: "1.0.0" });
|
|
4
|
-
app.ontoolresult = (result) => {
|
|
5
|
-
const text = result.content?.find((c) => c.type === "text")?.text;
|
|
6
|
-
if (text) {
|
|
7
|
-
try {
|
|
8
|
-
const invoice = JSON.parse(text);
|
|
9
|
-
renderInvoice(invoice);
|
|
10
|
-
}
|
|
11
|
-
catch (e) {
|
|
12
|
-
appEl.innerHTML = `<div class="loading">Error parsing invoice data</div>`;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
app.connect();
|
|
17
|
-
function getStatusInfo(statusId) {
|
|
18
|
-
switch (statusId) {
|
|
19
|
-
case 7: return { label: "Draft", className: "status-draft" };
|
|
20
|
-
case 8: return { label: "Pending", className: "status-pending" };
|
|
21
|
-
case 9: return { label: "Sent", className: "status-sent" };
|
|
22
|
-
case 16: return { label: "Paid", className: "status-paid" };
|
|
23
|
-
case 17: return { label: "Overdue", className: "status-overdue" };
|
|
24
|
-
case 19: return { label: "Cancelled", className: "status-cancelled" };
|
|
25
|
-
default: return { label: `Status ${statusId}`, className: "status-draft" };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function formatCurrency(amount, currencyId) {
|
|
29
|
-
const value = typeof amount === "string" ? parseFloat(amount) : amount;
|
|
30
|
-
const currency = currencyId === 1 ? "CHF" : currencyId === 2 ? "EUR" : "USD";
|
|
31
|
-
return `${currency} ${value.toFixed(2)}`;
|
|
32
|
-
}
|
|
33
|
-
function formatDate(dateStr) {
|
|
34
|
-
if (!dateStr)
|
|
35
|
-
return "";
|
|
36
|
-
const date = new Date(dateStr);
|
|
37
|
-
return date.toLocaleDateString("de-CH", { day: "2-digit", month: "2-digit", year: "numeric" });
|
|
38
|
-
}
|
|
39
|
-
function renderInvoice(invoice) {
|
|
40
|
-
const status = getStatusInfo(invoice.kb_item_status_id);
|
|
41
|
-
const positions = invoice.positions || [];
|
|
42
|
-
appEl.className = "invoice";
|
|
43
|
-
appEl.innerHTML = `
|
|
44
|
-
<div class="header">
|
|
45
|
-
<div class="header-left">
|
|
46
|
-
<h1>Invoice ${invoice.document_nr}</h1>
|
|
47
|
-
<p class="title">${invoice.title || ""}</p>
|
|
48
|
-
</div>
|
|
49
|
-
<div class="header-right">
|
|
50
|
-
<div class="dates">
|
|
51
|
-
<div>Issue: ${formatDate(invoice.is_valid_from)}</div>
|
|
52
|
-
<div>Due: ${formatDate(invoice.is_valid_to)}</div>
|
|
53
|
-
</div>
|
|
54
|
-
<span class="status-badge ${status.className}">${status.label}</span>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
|
|
58
|
-
<div class="contact-section">
|
|
59
|
-
<h2>Bill To</h2>
|
|
60
|
-
<div class="name">${invoice.contact_address?.split("\n")[0] || `Contact #${invoice.contact_id}`}</div>
|
|
61
|
-
<div class="email">${invoice.contact_address?.split("\n").slice(1).join(", ") || ""}</div>
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
<table class="line-items">
|
|
65
|
-
<thead>
|
|
66
|
-
<tr>
|
|
67
|
-
<th>Description</th>
|
|
68
|
-
<th>Qty</th>
|
|
69
|
-
<th>Price</th>
|
|
70
|
-
<th>Amount</th>
|
|
71
|
-
</tr>
|
|
72
|
-
</thead>
|
|
73
|
-
<tbody>
|
|
74
|
-
${positions.length > 0 ? positions.map((p) => `
|
|
75
|
-
<tr>
|
|
76
|
-
<td>${p.text}</td>
|
|
77
|
-
<td>${p.amount}</td>
|
|
78
|
-
<td>${formatCurrency(p.unit_price, invoice.currency_id)}</td>
|
|
79
|
-
<td>${formatCurrency(p.amount * p.unit_price * (1 - (p.discount_in_percent || 0) / 100), invoice.currency_id)}</td>
|
|
80
|
-
</tr>
|
|
81
|
-
`).join("") : `
|
|
82
|
-
<tr>
|
|
83
|
-
<td colspan="4" style="text-align: center; color: #6b7280;">No line items available</td>
|
|
84
|
-
</tr>
|
|
85
|
-
`}
|
|
86
|
-
</tbody>
|
|
87
|
-
</table>
|
|
88
|
-
|
|
89
|
-
<div class="total-section">
|
|
90
|
-
<div class="total-row">
|
|
91
|
-
<span class="total-label">Total:</span>
|
|
92
|
-
<span class="total-amount">${formatCurrency(invoice.total_gross, invoice.currency_id)}</span>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
`;
|
|
96
|
-
}
|