@lanten-ai/mcp-server 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.
- package/README.md +95 -0
- package/build/client.js +36 -0
- package/build/index.js +24 -0
- package/build/tools/tenants.js +124 -0
- package/build/tools/units.js +128 -0
- package/build/tools/work-orders.js +142 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @lanten-ai/mcp-server
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server that connects AI assistants to the [Lanten](https://lanten.co) property management API. Use it with Claude, Cursor, Zed, or any MCP-compatible client to manage tenants, units, and work orders using natural language.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
1. **Get an API key** — in your Lanten dashboard go to **Settings → Developers** and create a key.
|
|
8
|
+
|
|
9
|
+
2. **Add to your MCP client config:**
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"lanten": {
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": ["-y", "@lanten-ai/mcp-server"],
|
|
17
|
+
"env": {
|
|
18
|
+
"LANTEN_API_KEY": "lk_your_api_key_here"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For **Claude Desktop** this file lives at:
|
|
26
|
+
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
27
|
+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
28
|
+
|
|
29
|
+
For **Claude Code** run: `claude mcp add lanten -e LANTEN_API_KEY=lk_your_key -- npx -y @lanten-ai/mcp-server`
|
|
30
|
+
|
|
31
|
+
3. **Restart your client** and start chatting:
|
|
32
|
+
|
|
33
|
+
> *"List all open work orders for Flat 4, 12 Oak Street"*
|
|
34
|
+
> *"Create a new tenant: Jane Smith, jane@example.com, assign her to unit abc-123"*
|
|
35
|
+
> *"Mark work order WO-214 as completed"*
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Available tools
|
|
40
|
+
|
|
41
|
+
### Tenants
|
|
42
|
+
|
|
43
|
+
| Tool | Description |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `list_tenants` | List tenants with optional search and pagination |
|
|
46
|
+
| `get_tenant` | Get a single tenant by ID |
|
|
47
|
+
| `create_tenant` | Create a tenant, optionally linked to a unit |
|
|
48
|
+
| `update_tenant` | Update tenant fields (partial update) |
|
|
49
|
+
| `delete_tenant` | Permanently delete a tenant |
|
|
50
|
+
|
|
51
|
+
### Units
|
|
52
|
+
|
|
53
|
+
| Tool | Description |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `list_units` | List property units with optional address search |
|
|
56
|
+
| `get_unit` | Get a single unit by ID |
|
|
57
|
+
| `create_unit` | Create a property unit |
|
|
58
|
+
| `update_unit` | Update unit fields (partial update) |
|
|
59
|
+
| `delete_unit` | Permanently delete a unit |
|
|
60
|
+
|
|
61
|
+
### Work Orders
|
|
62
|
+
|
|
63
|
+
| Tool | Description |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `list_work_orders` | List work orders; filter by status, priority, tenant, or unit |
|
|
66
|
+
| `get_work_order` | Get a single work order by ID |
|
|
67
|
+
| `create_work_order` | Create a work order linked to a tenant/unit |
|
|
68
|
+
| `update_work_order` | Update a work order (status, priority, etc.) |
|
|
69
|
+
| `delete_work_order` | Permanently delete a work order |
|
|
70
|
+
|
|
71
|
+
**Priority levels:** `0` Emergency · `1` High · `2` Medium · `3` Low
|
|
72
|
+
**Statuses:** `reported` · `in_progress` · `completed` · `cancelled`
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Environment variables
|
|
77
|
+
|
|
78
|
+
| Variable | Required | Description |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `LANTEN_API_KEY` | ✅ | Your API key from Settings → Developers |
|
|
81
|
+
| `LANTEN_API_URL` | — | Override the base URL (e.g. for self-hosted instances). Defaults to `https://app.lanten.co/api/open/v1` |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Developing locally
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
git clone https://github.com/lanten/mcp-server
|
|
89
|
+
cd mcp-server
|
|
90
|
+
npm install
|
|
91
|
+
npm run build
|
|
92
|
+
|
|
93
|
+
# Test with the MCP Inspector
|
|
94
|
+
LANTEN_API_KEY=lk_your_key npm run inspector
|
|
95
|
+
```
|
package/build/client.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class LantenClient {
|
|
2
|
+
baseUrl;
|
|
3
|
+
apiKey;
|
|
4
|
+
constructor(baseUrl, apiKey) {
|
|
5
|
+
this.baseUrl = baseUrl;
|
|
6
|
+
this.apiKey = apiKey;
|
|
7
|
+
}
|
|
8
|
+
async request(method, path, options = {}) {
|
|
9
|
+
let url = `${this.baseUrl}${path}`;
|
|
10
|
+
if (options.query) {
|
|
11
|
+
const params = new URLSearchParams();
|
|
12
|
+
for (const [key, value] of Object.entries(options.query)) {
|
|
13
|
+
if (value !== undefined && value !== '') {
|
|
14
|
+
params.set(key, String(value));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const qs = params.toString();
|
|
18
|
+
if (qs)
|
|
19
|
+
url += `?${qs}`;
|
|
20
|
+
}
|
|
21
|
+
const res = await fetch(url, {
|
|
22
|
+
method,
|
|
23
|
+
headers: {
|
|
24
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
},
|
|
27
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
28
|
+
});
|
|
29
|
+
const json = (await res.json());
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
const message = typeof json.error === 'string' ? json.error : `HTTP ${res.status} ${res.statusText}`;
|
|
32
|
+
throw new Error(message);
|
|
33
|
+
}
|
|
34
|
+
return json;
|
|
35
|
+
}
|
|
36
|
+
}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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 { LantenClient } from './client.js';
|
|
5
|
+
import { registerTenantTools } from './tools/tenants.js';
|
|
6
|
+
import { registerUnitTools } from './tools/units.js';
|
|
7
|
+
import { registerWorkOrderTools } from './tools/work-orders.js';
|
|
8
|
+
const apiKey = process.env.LANTEN_API_KEY;
|
|
9
|
+
if (!apiKey) {
|
|
10
|
+
console.error('Error: LANTEN_API_KEY environment variable is required.');
|
|
11
|
+
console.error('Get an API key from your Lanten dashboard: Settings → Developers');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
const baseUrl = (process.env.LANTEN_API_URL ?? 'https://app.lanten.co/api/open/v1').replace(/\/$/, '');
|
|
15
|
+
const client = new LantenClient(baseUrl, apiKey);
|
|
16
|
+
const server = new McpServer({
|
|
17
|
+
name: 'lanten',
|
|
18
|
+
version: '0.1.0',
|
|
19
|
+
});
|
|
20
|
+
registerTenantTools(server, client);
|
|
21
|
+
registerUnitTools(server, client);
|
|
22
|
+
registerWorkOrderTools(server, client);
|
|
23
|
+
const transport = new StdioServerTransport();
|
|
24
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function registerTenantTools(server, client) {
|
|
3
|
+
// ── List ────────────────────────────────────────────────────────────────────
|
|
4
|
+
server.registerTool('list_tenants', {
|
|
5
|
+
description: 'List tenants for the account. Supports free-text search across name, email, phone, and unit address. Returns paginated results.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
search: z.string().optional().describe('Search across name, email, phone, unit address'),
|
|
8
|
+
page: z.number().int().min(1).optional().default(1).describe('Page number (default 1)'),
|
|
9
|
+
pageSize: z
|
|
10
|
+
.number()
|
|
11
|
+
.int()
|
|
12
|
+
.min(1)
|
|
13
|
+
.max(50)
|
|
14
|
+
.optional()
|
|
15
|
+
.default(20)
|
|
16
|
+
.describe('Results per page (max 50, default 20)'),
|
|
17
|
+
},
|
|
18
|
+
}, async ({ search, page, pageSize }) => {
|
|
19
|
+
try {
|
|
20
|
+
const result = await client.request('GET', '/tenants', {
|
|
21
|
+
query: { search, page, pageSize },
|
|
22
|
+
});
|
|
23
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
28
|
+
isError: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
// ── Get ─────────────────────────────────────────────────────────────────────
|
|
33
|
+
server.registerTool('get_tenant', {
|
|
34
|
+
description: 'Get a single tenant by ID, including their linked units.',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
id: z.string().uuid().describe('Tenant UUID'),
|
|
37
|
+
},
|
|
38
|
+
}, async ({ id }) => {
|
|
39
|
+
try {
|
|
40
|
+
const result = await client.request('GET', `/tenants/${id}`);
|
|
41
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
46
|
+
isError: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
// ── Create ──────────────────────────────────────────────────────────────────
|
|
51
|
+
server.registerTool('create_tenant', {
|
|
52
|
+
description: 'Create a new tenant. Optionally assign them to an existing unit.',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
firstName: z.string().min(1).max(160).describe('First name'),
|
|
55
|
+
lastName: z.string().min(1).max(160).describe('Last name'),
|
|
56
|
+
email: z.string().email().optional().describe('Email address'),
|
|
57
|
+
phoneNumber: z.string().optional().describe('Phone number'),
|
|
58
|
+
notes: z.string().optional().describe('Internal notes'),
|
|
59
|
+
unitId: z
|
|
60
|
+
.string()
|
|
61
|
+
.uuid()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('UUID of an existing unit to assign this tenant to'),
|
|
64
|
+
},
|
|
65
|
+
}, async ({ firstName, lastName, email, phoneNumber, notes, unitId }) => {
|
|
66
|
+
try {
|
|
67
|
+
const result = await client.request('POST', '/tenants', {
|
|
68
|
+
body: { firstName, lastName, email, phoneNumber, notes, unitId },
|
|
69
|
+
});
|
|
70
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
75
|
+
isError: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// ── Update ──────────────────────────────────────────────────────────────────
|
|
80
|
+
server.registerTool('update_tenant', {
|
|
81
|
+
description: 'Update an existing tenant. Only supplied fields are changed.',
|
|
82
|
+
inputSchema: {
|
|
83
|
+
id: z.string().uuid().describe('Tenant UUID'),
|
|
84
|
+
firstName: z.string().min(1).max(160).optional().describe('Updated first name'),
|
|
85
|
+
lastName: z.string().min(1).max(160).optional().describe('Updated last name'),
|
|
86
|
+
email: z.string().email().nullable().optional().describe('Updated email (null to clear)'),
|
|
87
|
+
phoneNumber: z
|
|
88
|
+
.string()
|
|
89
|
+
.nullable()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe('Updated phone number (null to clear)'),
|
|
92
|
+
notes: z.string().nullable().optional().describe('Updated notes (null to clear)'),
|
|
93
|
+
},
|
|
94
|
+
}, async ({ id, ...fields }) => {
|
|
95
|
+
try {
|
|
96
|
+
const result = await client.request('PUT', `/tenants/${id}`, { body: fields });
|
|
97
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
102
|
+
isError: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
// ── Delete ──────────────────────────────────────────────────────────────────
|
|
107
|
+
server.registerTool('delete_tenant', {
|
|
108
|
+
description: 'Permanently delete a tenant. This cannot be undone.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
id: z.string().uuid().describe('Tenant UUID'),
|
|
111
|
+
},
|
|
112
|
+
}, async ({ id }) => {
|
|
113
|
+
try {
|
|
114
|
+
const result = await client.request('DELETE', `/tenants/${id}`);
|
|
115
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
120
|
+
isError: true,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function registerUnitTools(server, client) {
|
|
3
|
+
// ── List ────────────────────────────────────────────────────────────────────
|
|
4
|
+
server.registerTool('list_units', {
|
|
5
|
+
description: 'List property units for the account. Supports address search. Returns paginated results including current tenants.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
search: z.string().optional().describe('Search by address'),
|
|
8
|
+
page: z.number().int().min(1).optional().default(1).describe('Page number (default 1)'),
|
|
9
|
+
pageSize: z
|
|
10
|
+
.number()
|
|
11
|
+
.int()
|
|
12
|
+
.min(1)
|
|
13
|
+
.max(50)
|
|
14
|
+
.optional()
|
|
15
|
+
.default(20)
|
|
16
|
+
.describe('Results per page (max 50, default 20)'),
|
|
17
|
+
},
|
|
18
|
+
}, async ({ search, page, pageSize }) => {
|
|
19
|
+
try {
|
|
20
|
+
const result = await client.request('GET', '/units', {
|
|
21
|
+
query: { search, page, pageSize },
|
|
22
|
+
});
|
|
23
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
28
|
+
isError: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
// ── Get ─────────────────────────────────────────────────────────────────────
|
|
33
|
+
server.registerTool('get_unit', {
|
|
34
|
+
description: 'Get a single property unit by ID, including current tenants.',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
id: z.string().uuid().describe('Unit UUID'),
|
|
37
|
+
},
|
|
38
|
+
}, async ({ id }) => {
|
|
39
|
+
try {
|
|
40
|
+
const result = await client.request('GET', `/units/${id}`);
|
|
41
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
46
|
+
isError: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
// ── Create ──────────────────────────────────────────────────────────────────
|
|
51
|
+
server.registerTool('create_unit', {
|
|
52
|
+
description: 'Create a new property unit.',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
addressLine1: z.string().min(1).max(260).describe('Street address line 1'),
|
|
55
|
+
addressLine2: z
|
|
56
|
+
.string()
|
|
57
|
+
.max(260)
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('Street address line 2 (flat number, etc.)'),
|
|
60
|
+
city: z.string().min(1).max(160).describe('City or town'),
|
|
61
|
+
county: z.string().max(160).optional().describe('County or province'),
|
|
62
|
+
country: z.string().min(1).max(160).describe('Country'),
|
|
63
|
+
postcode: z.string().min(1).max(80).describe('Postal / zip code'),
|
|
64
|
+
area: z.string().max(160).optional().describe('Area or region label (e.g. "North Zone")'),
|
|
65
|
+
},
|
|
66
|
+
}, async ({ addressLine1, addressLine2, city, county, country, postcode, area }) => {
|
|
67
|
+
try {
|
|
68
|
+
const result = await client.request('POST', '/units', {
|
|
69
|
+
body: { addressLine1, addressLine2, city, county, country, postcode, area },
|
|
70
|
+
});
|
|
71
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
76
|
+
isError: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
// ── Update ──────────────────────────────────────────────────────────────────
|
|
81
|
+
server.registerTool('update_unit', {
|
|
82
|
+
description: 'Update an existing property unit. Only supplied fields are changed.',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
id: z.string().uuid().describe('Unit UUID'),
|
|
85
|
+
addressLine1: z.string().min(1).max(260).optional().describe('Updated street address'),
|
|
86
|
+
addressLine2: z
|
|
87
|
+
.string()
|
|
88
|
+
.max(260)
|
|
89
|
+
.nullable()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe('Updated address line 2 (null to clear)'),
|
|
92
|
+
city: z.string().min(1).max(160).optional().describe('Updated city'),
|
|
93
|
+
county: z.string().max(160).optional().describe('Updated county'),
|
|
94
|
+
country: z.string().min(1).max(160).optional().describe('Updated country'),
|
|
95
|
+
postcode: z.string().min(1).max(80).optional().describe('Updated postcode'),
|
|
96
|
+
area: z.string().max(160).nullable().optional().describe('Updated area (null to clear)'),
|
|
97
|
+
},
|
|
98
|
+
}, async ({ id, ...fields }) => {
|
|
99
|
+
try {
|
|
100
|
+
const result = await client.request('PUT', `/units/${id}`, { body: fields });
|
|
101
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
106
|
+
isError: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
// ── Delete ──────────────────────────────────────────────────────────────────
|
|
111
|
+
server.registerTool('delete_unit', {
|
|
112
|
+
description: 'Permanently delete a property unit. This cannot be undone.',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
id: z.string().uuid().describe('Unit UUID'),
|
|
115
|
+
},
|
|
116
|
+
}, async ({ id }) => {
|
|
117
|
+
try {
|
|
118
|
+
const result = await client.request('DELETE', `/units/${id}`);
|
|
119
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
124
|
+
isError: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const STATUS = z
|
|
3
|
+
.enum(['reported', 'in_progress', 'completed', 'cancelled'])
|
|
4
|
+
.describe('Work order status');
|
|
5
|
+
const PRIORITY = z
|
|
6
|
+
.enum(['0', '1', '2', '3'])
|
|
7
|
+
.describe('Priority: 0 = Emergency, 1 = High, 2 = Medium, 3 = Low');
|
|
8
|
+
export function registerWorkOrderTools(server, client) {
|
|
9
|
+
// ── List ────────────────────────────────────────────────────────────────────
|
|
10
|
+
server.registerTool('list_work_orders', {
|
|
11
|
+
description: 'List work orders (maintenance issues) for the account. Filter by status, priority, tenant, or unit. Supports search by title or code.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
search: z.string().optional().describe('Search by title or work order code (e.g. WO-101)'),
|
|
14
|
+
status: STATUS.optional(),
|
|
15
|
+
priority: PRIORITY.optional(),
|
|
16
|
+
tenantId: z.string().uuid().optional().describe('Filter by tenant UUID'),
|
|
17
|
+
unitId: z.string().uuid().optional().describe('Filter by unit UUID'),
|
|
18
|
+
page: z.number().int().min(1).optional().default(1).describe('Page number (default 1)'),
|
|
19
|
+
pageSize: z
|
|
20
|
+
.number()
|
|
21
|
+
.int()
|
|
22
|
+
.min(1)
|
|
23
|
+
.max(50)
|
|
24
|
+
.optional()
|
|
25
|
+
.default(20)
|
|
26
|
+
.describe('Results per page (max 50, default 20)'),
|
|
27
|
+
},
|
|
28
|
+
}, async ({ search, status, priority, tenantId, unitId, page, pageSize }) => {
|
|
29
|
+
try {
|
|
30
|
+
const result = await client.request('GET', '/work-orders', {
|
|
31
|
+
query: { search, status, priority, tenantId, unitId, page, pageSize },
|
|
32
|
+
});
|
|
33
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
38
|
+
isError: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
// ── Get ─────────────────────────────────────────────────────────────────────
|
|
43
|
+
server.registerTool('get_work_order', {
|
|
44
|
+
description: 'Get a single work order by ID, including tenant, unit, and attachments.',
|
|
45
|
+
inputSchema: {
|
|
46
|
+
id: z.string().uuid().describe('Work order UUID'),
|
|
47
|
+
},
|
|
48
|
+
}, async ({ id }) => {
|
|
49
|
+
try {
|
|
50
|
+
const result = await client.request('GET', `/work-orders/${id}`);
|
|
51
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// ── Create ──────────────────────────────────────────────────────────────────
|
|
61
|
+
server.registerTool('create_work_order', {
|
|
62
|
+
description: 'Create a new work order (maintenance issue). Optionally link to a tenant and/or unit.',
|
|
63
|
+
inputSchema: {
|
|
64
|
+
title: z.string().min(1).max(260).describe('Short title describing the issue'),
|
|
65
|
+
description: z.string().min(1).describe('Full description of the issue'),
|
|
66
|
+
priority: PRIORITY.optional().default('2'),
|
|
67
|
+
tenantId: z.string().uuid().optional().describe('UUID of the tenant who reported it'),
|
|
68
|
+
unitId: z.string().uuid().optional().describe('UUID of the property unit affected'),
|
|
69
|
+
availability: z
|
|
70
|
+
.string()
|
|
71
|
+
.max(260)
|
|
72
|
+
.optional()
|
|
73
|
+
.describe("Free-text tenant availability window (e.g. 'weekday mornings')"),
|
|
74
|
+
},
|
|
75
|
+
}, async ({ title, description, priority, tenantId, unitId, availability }) => {
|
|
76
|
+
try {
|
|
77
|
+
const result = await client.request('POST', '/work-orders', {
|
|
78
|
+
body: { title, description, priority, tenantId, unitId, availability },
|
|
79
|
+
});
|
|
80
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
85
|
+
isError: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
// ── Update ──────────────────────────────────────────────────────────────────
|
|
90
|
+
server.registerTool('update_work_order', {
|
|
91
|
+
description: 'Update an existing work order. Only supplied fields are changed. Setting status to "completed" automatically records the resolved time.',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
id: z.string().uuid().describe('Work order UUID'),
|
|
94
|
+
title: z.string().min(1).max(260).optional().describe('Updated title'),
|
|
95
|
+
description: z.string().min(1).optional().describe('Updated description'),
|
|
96
|
+
status: STATUS.optional(),
|
|
97
|
+
priority: PRIORITY.optional(),
|
|
98
|
+
tenantId: z
|
|
99
|
+
.string()
|
|
100
|
+
.uuid()
|
|
101
|
+
.nullable()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe('Updated tenant UUID (null to unlink)'),
|
|
104
|
+
unitId: z
|
|
105
|
+
.string()
|
|
106
|
+
.uuid()
|
|
107
|
+
.nullable()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe('Updated unit UUID (null to unlink)'),
|
|
110
|
+
availability: z.string().max(260).optional().describe('Updated availability window'),
|
|
111
|
+
},
|
|
112
|
+
}, async ({ id, ...fields }) => {
|
|
113
|
+
try {
|
|
114
|
+
const result = await client.request('PUT', `/work-orders/${id}`, { body: fields });
|
|
115
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
120
|
+
isError: true,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// ── Delete ──────────────────────────────────────────────────────────────────
|
|
125
|
+
server.registerTool('delete_work_order', {
|
|
126
|
+
description: 'Permanently delete a work order. This cannot be undone.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
id: z.string().uuid().describe('Work order UUID'),
|
|
129
|
+
},
|
|
130
|
+
}, async ({ id }) => {
|
|
131
|
+
try {
|
|
132
|
+
const result = await client.request('DELETE', `/work-orders/${id}`);
|
|
133
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
138
|
+
isError: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lanten-ai/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for the Lanten property management API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lanten-mcp": "build/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"build"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc && chmod 755 build/index.js",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"inspector": "npx @modelcontextprotocol/inspector node ./build/index.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
19
|
+
"zod": "^3.24.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20",
|
|
23
|
+
"typescript": "^5"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
}
|
|
28
|
+
}
|