flatin-mcp 0.0.0-stage → 1.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/LICENSE +21 -0
- package/README.md +103 -2
- package/bin/flatin-mcp.js +215 -0
- package/package.json +40 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 flatin.pt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,104 @@
|
|
|
1
|
-
#
|
|
1
|
+
# flatin.pt MCP server — buying a home in Portugal
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server for the questions people ask before buying a home in Portugal: **which new-build developments fit a budget**, **where to live in a city, area by area**, **what a municipality looks like in numbers**, and **what the purchase and the yearly property tax cost** — with the flatin.pt page link and the source of every number.
|
|
4
|
+
|
|
5
|
+
It is run by [flatin.pt](https://flatin.pt/en/) and listed in the official MCP Registry as [`pt.flatin/property-taxes`](https://registry.modelcontextprotocol.io/v0.1/servers/pt.flatin%2Fproperty-taxes/versions/latest) (the name predates the wider scope; version 2.1.0 covers new-builds and places and publishes an output schema for every tool).
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
You: T2 new-builds in Vila Nova de Gaia under 400 000 €? And what would I pay on top?
|
|
9
|
+
|
|
10
|
+
Claude: [search_newbuilds municipality="Vila Nova de Gaia" typology="T2" price_max=400000]
|
|
11
|
+
[imt_calculate value="380000"]
|
|
12
|
+
Lists the matching projects with their price per typology, stage,
|
|
13
|
+
delivery date and flatin.pt page; then IMT, stamp duty and the
|
|
14
|
+
registration on top of the price, with the Autoridade Tributária
|
|
15
|
+
tables as the source.
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Tools
|
|
19
|
+
|
|
20
|
+
| Tool | What it answers |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `imt_calculate` | What a purchase costs on top of the price: IMT (transfer tax), Imposto do Selo (stamp duty) and the published price of the property registration (Casa Pronta). Official 2026 tables, including the reduced IMT table and the stamp duty deduction for buyers aged 35 or under. Both taxes are charged on the higher of the contract price and the VPT: pass `vpt` when it is known. |
|
|
23
|
+
| `imi_rate` | The yearly property tax (IMI) rate of one municipality, by name or four-digit code, with its history since 2023. Where a municipality sets no single rate (it is set per parish), the answer says so instead of returning zero. |
|
|
24
|
+
| `imi_annual_cost` | Yearly IMI for an urban property with a given taxable value (VPT). VPT is the tax authority's valuation, not the purchase price. |
|
|
25
|
+
| `request_consultation` | Sends the person's request to a licensed real estate agency (AMI), in two steps: `mode=prepare` checks the details and returns a summary, the GDPR consent text in their language and a confirmation token; `mode=submit` sends it only with that token, `consent=true` and the text version, after the person has read the text and agreed. `mode=link` (default) returns just the form link. |
|
|
26
|
+
| `subscribe_digest` | Subscribes the person's email to new-build developments matching a filter (daily) or to newly licensed construction in a municipality (weekly). It only sends a confirmation email: nothing is active until they click the link. |
|
|
27
|
+
| `where_to_live` | Where to live in a Portuguese city or region, area by area: the verdict of a flatin.pt guide (live, depends, avoid) with the reason, who each area suits, noise in plain words and dated, sourced safety notes. Without `city`, the list of guides. |
|
|
28
|
+
| `municipality_stats` | Key figures for one of the 308 municipalities: IMI rate and history, INE median sale price per m², purchasing power, population, licensed buildings and dwellings, construction activity, new-builds in the catalogue — each with source and date. |
|
|
29
|
+
| `compare_municipalities` | The same figures for two to five municipalities side by side. |
|
|
30
|
+
| `construction_barometer` | Construction activity: monthly municipal building-works cases where we collect them, plus INE's yearly licensed buildings and new dwellings for any municipality. |
|
|
31
|
+
| `glossary_lookup` | A Portuguese home-buying or tax term (CPCV, escritura, caderneta predial, VPT…) explained, with the article of law and the date it was checked. |
|
|
32
|
+
| `search_newbuilds` | New-build housing projects on flatin.pt by municipality, parish, typology (T0–T5), budget, stage, delivery year and energy class: price per typology, stage, delivery date, developer, coordinates and the project page link. |
|
|
33
|
+
| `get_newbuild` | One new-build project: units and prices per typology with price history, delivery promises, building permits, the purchase cost (IMT, stamp duty, registration) and what is nearby, with the page link in seven languages. |
|
|
34
|
+
|
|
35
|
+
Every successful answer carries an `attribution` field — who computed it, from which table and year, and a link to the primary source — and, where there is one, the canonical flatin.pt page URL. Please show both to the user: numbers without a source cannot be checked.
|
|
36
|
+
|
|
37
|
+
The two write tools (`request_consultation`, `subscribe_digest`) act only with the person's explicit consent: `prepare` returns the consent text and a token and sends nothing; `submit` needs that token and `consent=true`. Municipalities can be named in Portuguese or English (Lisboa or Lisbon), by slug or by four-digit code.
|
|
38
|
+
|
|
39
|
+
## Connect
|
|
40
|
+
|
|
41
|
+
The server is **remote** — nothing to install. Point your client at:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
https://flatin.pt/mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Claude Code**
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
claude mcp add --transport http flatin https://flatin.pt/mcp
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Clients that can only start a local process** (stdio) — use the bridge in this repository. It needs Node.js 18.17 or newer and has no dependencies:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
claude mcp add flatin -- npx -y gitlab:flatin-pt-group/mcp
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"flatin": {
|
|
63
|
+
"command": "npx",
|
|
64
|
+
"args": ["-y", "gitlab:flatin-pt-group/mcp"]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The bridge forwards each JSON-RPC message to `https://flatin.pt/mcp` and writes the answer back. It stores nothing and sends nothing except what your client sends. Set `FLATIN_MCP_URL` to point it elsewhere.
|
|
71
|
+
|
|
72
|
+
**Docker**
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
docker build -t flatin-mcp . && docker run -i --rm flatin-mcp
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## The data behind it
|
|
79
|
+
|
|
80
|
+
- **New-build developments**: prices per typology and delivery dates as published by developers, with the date they were checked, and building permits from municipal open-data portals — [catalogue](https://flatin.pt/en/new-developments/).
|
|
81
|
+
- **Where to live**: flatin.pt city guides area by area, and municipality figures from INE (median sale price per m², purchasing power, population, licensed construction).
|
|
82
|
+
- **IMI rates, all 308 municipalities**, checked line by line against the Autoridade Tributária portal — [open data page](https://flatin.pt/en/tools/imi-open-data/), [CSV](https://flatin.pt/api/v1/imi/rates.csv), licence CC BY 4.0.
|
|
83
|
+
- **IMT and stamp duty** from the official 2026 tables — the same numbers as the [IMT calculator](https://flatin.pt/en/tools/imt-calculator/).
|
|
84
|
+
- **REST API** with the same functions: [API and data](https://flatin.pt/en/tools/api-and-data/) · [OpenAPI schema](https://flatin.pt/api/openapi.json).
|
|
85
|
+
|
|
86
|
+
The MCP server, the API and the website call the same code, so the three never disagree by a cent.
|
|
87
|
+
|
|
88
|
+
## Guides
|
|
89
|
+
|
|
90
|
+
- [Buying property in Portugal: costs and taxes](https://flatin.pt/en/guides/buying-property-portugal-costs-taxes/)
|
|
91
|
+
- [NIF and residency for foreign buyers](https://flatin.pt/en/guides/nif-residency-foreign-buyers/)
|
|
92
|
+
- In Portuguese: [Comprar casa: custos e impostos](https://flatin.pt/pt/guias/comprar-casa-custos-impostos/) · [Calculadora de IMT](https://flatin.pt/pt/ferramentas/calculadora-imt/)
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
node --test
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This repository is published from the flatin.pt codebase; issues and suggestions are welcome here.
|
|
101
|
+
|
|
102
|
+
## Licence
|
|
103
|
+
|
|
104
|
+
MIT — see [LICENSE](LICENSE). The data returned by the server is licensed separately under CC BY 4.0.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* flatin-mcp — a stdio bridge to the flatin.pt MCP server.
|
|
4
|
+
*
|
|
5
|
+
* The server itself is remote (https://flatin.pt/mcp, streamable HTTP) and
|
|
6
|
+
* stateless. Some MCP clients can only start a local process and talk to it
|
|
7
|
+
* over stdin/stdout; this bridge is that process. It forwards every JSON-RPC
|
|
8
|
+
* message unchanged and writes the answer back. It adds nothing, stores
|
|
9
|
+
* nothing and sends nothing except what the client sent.
|
|
10
|
+
*
|
|
11
|
+
* Rules the bridge keeps:
|
|
12
|
+
* - stdout carries JSON-RPC only, one message per line; diagnostics go to stderr;
|
|
13
|
+
* - a notification (no id) never produces output;
|
|
14
|
+
* - every request with an id gets exactly one answer, even when the network,
|
|
15
|
+
* a proxy or the server fails — a client must never wait forever;
|
|
16
|
+
* - the protocol version is forwarded: the one negotiated in `initialize`,
|
|
17
|
+
* or the one a client states in a request's `_meta`.
|
|
18
|
+
*/
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const readline = require('node:readline');
|
|
22
|
+
const { version: VERSION } = require('../package.json');
|
|
23
|
+
|
|
24
|
+
const ENDPOINT = process.env.FLATIN_MCP_URL || 'https://flatin.pt/mcp';
|
|
25
|
+
const TIMEOUT_MS = Number(process.env.FLATIN_MCP_TIMEOUT_MS || 30000);
|
|
26
|
+
|
|
27
|
+
const PARSE_ERROR = -32700;
|
|
28
|
+
const INTERNAL_ERROR = -32603;
|
|
29
|
+
|
|
30
|
+
let negotiatedVersion = null;
|
|
31
|
+
|
|
32
|
+
function log(message) {
|
|
33
|
+
process.stderr.write(`[flatin-mcp] ${message}\n`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function emit(message) {
|
|
37
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isObject(value) {
|
|
41
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasId(message) {
|
|
45
|
+
return isObject(message) && message.id !== undefined && message.id !== null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isJsonRpc(message) {
|
|
49
|
+
return isObject(message) && message.jsonrpc === '2.0';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function errorReply(id, code, text) {
|
|
53
|
+
return { jsonrpc: '2.0', id, error: { code, message: text } };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The requests in a message that are owed an answer. */
|
|
57
|
+
function idsOf(request) {
|
|
58
|
+
return (Array.isArray(request) ? request : [request]).filter(hasId).map((item) => item.id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A version the client states itself, e.g. in `params._meta`, when there was no handshake. */
|
|
62
|
+
function statedVersion(request) {
|
|
63
|
+
for (const item of Array.isArray(request) ? request : [request]) {
|
|
64
|
+
const meta = isObject(item) && isObject(item.params) && isObject(item.params._meta) ? item.params._meta : null;
|
|
65
|
+
if (!meta) continue;
|
|
66
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
67
|
+
if (/protocolversion$/i.test(key) && typeof value === 'string') return value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Split a server-sent events body into the JSON-RPC messages it carries. */
|
|
74
|
+
function parseEvents(text) {
|
|
75
|
+
const messages = [];
|
|
76
|
+
let data = [];
|
|
77
|
+
const flush = () => {
|
|
78
|
+
if (data.length) messages.push(JSON.parse(data.join('\n')));
|
|
79
|
+
data = [];
|
|
80
|
+
};
|
|
81
|
+
for (const line of text.split(/\r?\n/)) {
|
|
82
|
+
if (line === '') flush();
|
|
83
|
+
else if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /, ''));
|
|
84
|
+
}
|
|
85
|
+
flush();
|
|
86
|
+
return messages;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function post(request) {
|
|
90
|
+
const headers = {
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
Accept: 'application/json, text/event-stream',
|
|
93
|
+
'User-Agent': `flatin-mcp/${VERSION} (+https://gitlab.com/flatin-pt-group/mcp)`,
|
|
94
|
+
};
|
|
95
|
+
const version = negotiatedVersion || statedVersion(request);
|
|
96
|
+
if (version) headers['MCP-Protocol-Version'] = version;
|
|
97
|
+
|
|
98
|
+
const response = await fetch(ENDPOINT, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers,
|
|
101
|
+
body: JSON.stringify(request),
|
|
102
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
103
|
+
});
|
|
104
|
+
const text = await response.text();
|
|
105
|
+
if (response.status === 202 && !text) return { status: 202, messages: [] };
|
|
106
|
+
|
|
107
|
+
let messages;
|
|
108
|
+
try {
|
|
109
|
+
if ((response.headers.get('content-type') || '').includes('text/event-stream')) {
|
|
110
|
+
messages = parseEvents(text);
|
|
111
|
+
} else if (text) {
|
|
112
|
+
// A batch answer stays one message: it goes back to the client as a batch.
|
|
113
|
+
messages = [JSON.parse(text)];
|
|
114
|
+
} else {
|
|
115
|
+
messages = [];
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
throw new Error(`HTTP ${response.status}: the answer is not JSON`);
|
|
119
|
+
}
|
|
120
|
+
return { status: response.status, messages };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Turn what the server sent into what the client is owed.
|
|
125
|
+
*
|
|
126
|
+
* - A JSON-RPC answer passes through. If the server could not read the id
|
|
127
|
+
* (a refusal with `id: null` or no id), the bridge did read it and puts it
|
|
128
|
+
* back — for every id in a batch.
|
|
129
|
+
* - Anything that is not JSON-RPC (a proxy page, `{"detail": ...}`, an HTTP
|
|
130
|
+
* error body) becomes a JSON-RPC error for each waiting id.
|
|
131
|
+
*/
|
|
132
|
+
function answersFor(request, status, messages) {
|
|
133
|
+
const waiting = idsOf(request);
|
|
134
|
+
const out = [];
|
|
135
|
+
for (const message of messages) {
|
|
136
|
+
if (Array.isArray(message)) {
|
|
137
|
+
const items = message.filter(isJsonRpc);
|
|
138
|
+
if (items.length) out.push(items);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!isJsonRpc(message)) {
|
|
142
|
+
const detail = isObject(message) && (message.message || message.detail || message.error);
|
|
143
|
+
const text = `HTTP ${status}: ${typeof detail === 'string' ? detail : 'not a JSON-RPC answer'}`;
|
|
144
|
+
for (const id of waiting) out.push(errorReply(id, INTERNAL_ERROR, text));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if ((message.id === null || message.id === undefined) && message.error && waiting.length) {
|
|
148
|
+
for (const id of waiting) out.push({ ...message, id });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
out.push(message);
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function rememberVersion(request, answers) {
|
|
157
|
+
const requests = Array.isArray(request) ? request : [request];
|
|
158
|
+
const flat = answers.flatMap((answer) => (Array.isArray(answer) ? answer : [answer]));
|
|
159
|
+
for (const item of requests) {
|
|
160
|
+
if (!isObject(item) || item.method !== 'initialize') continue;
|
|
161
|
+
const answer = flat.find((a) => isObject(a) && a.id === item.id);
|
|
162
|
+
if (answer && answer.result && answer.result.protocolVersion) {
|
|
163
|
+
negotiatedVersion = answer.result.protocolVersion;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function handle(line) {
|
|
169
|
+
let request;
|
|
170
|
+
try {
|
|
171
|
+
request = JSON.parse(line);
|
|
172
|
+
} catch {
|
|
173
|
+
emit(errorReply(null, PARSE_ERROR, 'the message is not valid JSON'));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const waiting = idsOf(request);
|
|
177
|
+
try {
|
|
178
|
+
const { status, messages } = await post(request);
|
|
179
|
+
const answers = answersFor(request, status, messages);
|
|
180
|
+
rememberVersion(request, answers);
|
|
181
|
+
for (const answer of answers) emit(answer);
|
|
182
|
+
const answered = new Set(
|
|
183
|
+
answers.flatMap((a) => (Array.isArray(a) ? a : [a])).filter(hasId).map((a) => a.id),
|
|
184
|
+
);
|
|
185
|
+
const missing = waiting.filter((id) => !answered.has(id));
|
|
186
|
+
if (missing.length && status !== 202) {
|
|
187
|
+
for (const id of missing) emit(errorReply(id, INTERNAL_ERROR, `HTTP ${status}: no answer for this request`));
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const reason = error && error.name === 'TimeoutError' ? `no answer within ${TIMEOUT_MS} ms` : String(error && error.message);
|
|
191
|
+
log(`request failed: ${reason}`);
|
|
192
|
+
for (const id of waiting) emit(errorReply(id, INTERNAL_ERROR, `flatin.pt MCP server is unreachable: ${reason}`));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function main() {
|
|
197
|
+
const pending = new Set();
|
|
198
|
+
const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
199
|
+
input.on('line', (line) => {
|
|
200
|
+
if (!line.trim()) return;
|
|
201
|
+
const job = handle(line).finally(() => pending.delete(job));
|
|
202
|
+
pending.add(job);
|
|
203
|
+
});
|
|
204
|
+
// No process.exit(): on macOS stdout to a pipe is asynchronous, and exiting
|
|
205
|
+
// right after the last write can cut a large answer. The process ends by
|
|
206
|
+
// itself once stdin is closed and every request has been answered.
|
|
207
|
+
input.on('close', () => {
|
|
208
|
+
Promise.allSettled([...pending]).then(() => {
|
|
209
|
+
process.exitCode = 0;
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
log(`bridging stdio to ${ENDPOINT}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flatin-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "MCP stdio bridge to flatin.pt: buying a home in Portugal — new-build search, where-to-live guides, municipality data, IMT and IMI taxes.",
|
|
5
|
+
"mcpName": "pt.flatin/property-taxes",
|
|
6
|
+
"bin": {
|
|
7
|
+
"flatin-mcp": "bin/flatin-mcp.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.17"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"mcp-server",
|
|
24
|
+
"portugal",
|
|
25
|
+
"property-tax",
|
|
26
|
+
"imi",
|
|
27
|
+
"imt",
|
|
28
|
+
"real-estate",
|
|
29
|
+
"new-build",
|
|
30
|
+
"open-data"
|
|
31
|
+
],
|
|
32
|
+
"homepage": "https://flatin.pt/en/tools/api-and-data/",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://gitlab.com/flatin-pt-group/mcp.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://gitlab.com/flatin-pt-group/mcp/-/issues"
|
|
39
|
+
},
|
|
40
|
+
"author": "flatin.pt (https://flatin.pt)",
|
|
41
|
+
"license": "MIT"
|
|
42
|
+
}
|