evimnet-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -0
- package/chatgpt/YAYINLAMA-REHBERI.md +94 -0
- package/chatgpt/gpt-instructions.md +55 -0
- package/chatgpt/openapi.yaml +290 -0
- package/package.json +21 -0
- package/server.js +129 -0
- package/tools/agent.js +179 -0
- package/tools/lead.js +101 -0
- package/tools/listings.js +263 -0
- package/tools/location.js +169 -0
- package/tools/market.js +131 -0
- package/tools/nearby.js +107 -0
package/server.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Evimnet MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Türkiye gayrimenkul platformu Evimnet'i MCP uyumlu AI asistanlara bağlar.
|
|
6
|
+
* Claude Desktop, VS Code Copilot, Cursor ve MCP destekli her AI kullanabilir.
|
|
7
|
+
*
|
|
8
|
+
* Kullanım:
|
|
9
|
+
* npx evimnet-mcp
|
|
10
|
+
* node server.js
|
|
11
|
+
*
|
|
12
|
+
* Ortam değişkenleri:
|
|
13
|
+
* EVIMNET_BASE_URL — API base URL (varsayılan: https://evimnet.com)
|
|
14
|
+
* EVIMNET_API_KEY — Emlakçı araçları için API key (opsiyonel)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
18
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
19
|
+
import {
|
|
20
|
+
CallToolRequestSchema,
|
|
21
|
+
ListToolsRequestSchema,
|
|
22
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
23
|
+
|
|
24
|
+
import { listingTools, handleListingsTool } from './tools/listings.js';
|
|
25
|
+
import { locationTools, handleLocationTool } from './tools/location.js';
|
|
26
|
+
import { nearbyTools, handleNearbyTool } from './tools/nearby.js';
|
|
27
|
+
import { leadTools, handleLeadTool } from './tools/lead.js';
|
|
28
|
+
import { marketTools, handleMarketTool } from './tools/market.js';
|
|
29
|
+
import { agentTools, handleAgentTool } from './tools/agent.js';
|
|
30
|
+
|
|
31
|
+
// Tüm araçlar
|
|
32
|
+
const ALL_TOOLS = [
|
|
33
|
+
...listingTools,
|
|
34
|
+
...locationTools,
|
|
35
|
+
...nearbyTools,
|
|
36
|
+
...leadTools,
|
|
37
|
+
...marketTools,
|
|
38
|
+
...agentTools,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Araç → handler eşleşmesi
|
|
42
|
+
const TOOL_HANDLERS = {
|
|
43
|
+
// Listing araçları
|
|
44
|
+
search_properties: (a) => handleListingsTool('search_properties', a),
|
|
45
|
+
get_property_details: (a) => handleListingsTool('get_property_details', a),
|
|
46
|
+
|
|
47
|
+
// Konum araçları
|
|
48
|
+
get_locations: (a) => handleLocationTool('get_locations', a),
|
|
49
|
+
search_location: (a) => handleLocationTool('search_location', a),
|
|
50
|
+
|
|
51
|
+
// Çevre bilgisi
|
|
52
|
+
get_area_info: (a) => handleNearbyTool('get_area_info', a),
|
|
53
|
+
|
|
54
|
+
// Lead / başvuru
|
|
55
|
+
send_inquiry: (a) => handleLeadTool('send_inquiry', a),
|
|
56
|
+
|
|
57
|
+
// Piyasa / döviz
|
|
58
|
+
get_currency_rates: (a) => handleMarketTool('get_currency_rates', a),
|
|
59
|
+
get_platform_stats: (a) => handleMarketTool('get_platform_stats', a),
|
|
60
|
+
convert_price: (a) => handleMarketTool('convert_price', a),
|
|
61
|
+
|
|
62
|
+
// Emlakçı araçları (API key gerekli)
|
|
63
|
+
get_my_leads: (a) => handleAgentTool('get_my_leads', a),
|
|
64
|
+
update_lead_status: (a) => handleAgentTool('update_lead_status', a),
|
|
65
|
+
get_my_listings: (a) => handleAgentTool('get_my_listings', a),
|
|
66
|
+
get_my_stats: (a) => handleAgentTool('get_my_stats', a),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ─── Server oluştur ──────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
const server = new Server(
|
|
72
|
+
{
|
|
73
|
+
name: 'evimnet-mcp',
|
|
74
|
+
version: '1.0.0',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
capabilities: { tools: {} },
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// ─── Araç listesi ─────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
84
|
+
tools: ALL_TOOLS,
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
// ─── Araç çağrısı ─────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
90
|
+
const { name, arguments: args } = request.params;
|
|
91
|
+
|
|
92
|
+
const handler = TOOL_HANDLERS[name];
|
|
93
|
+
if (!handler) {
|
|
94
|
+
return {
|
|
95
|
+
content: [{
|
|
96
|
+
type: 'text',
|
|
97
|
+
text: `Bilinmeyen araç: ${name}`,
|
|
98
|
+
}],
|
|
99
|
+
isError: true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
return await handler(args || {});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
content: [{
|
|
108
|
+
type: 'text',
|
|
109
|
+
text: `Hata: ${error.message}`,
|
|
110
|
+
}],
|
|
111
|
+
isError: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ─── Başlat ───────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
async function main() {
|
|
119
|
+
const transport = new StdioServerTransport();
|
|
120
|
+
await server.connect(transport);
|
|
121
|
+
console.error('[Evimnet MCP] Server başlatıldı — stdio transport aktif');
|
|
122
|
+
console.error(`[Evimnet MCP] ${ALL_TOOLS.length} araç yüklendi`);
|
|
123
|
+
console.error(`[Evimnet MCP] Base URL: ${process.env.EVIMNET_BASE_URL || 'https://evimnet.com'}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
main().catch((err) => {
|
|
127
|
+
console.error('[Evimnet MCP] Başlatma hatası:', err);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
package/tools/agent.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Tools — Emlakçıya özel araçlar (API key zorunlu)
|
|
3
|
+
*
|
|
4
|
+
* Lead yönetimi, ilan performansı, dashboard istatistikleri.
|
|
5
|
+
* API key: Evimnet paneli → Ayarlar → API Anahtarları
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const BASE_URL = process.env.EVIMNET_BASE_URL || 'https://evimnet.com';
|
|
9
|
+
const API_KEY = process.env.EVIMNET_API_KEY;
|
|
10
|
+
|
|
11
|
+
function agentHeaders() {
|
|
12
|
+
if (!API_KEY) throw new Error('EVIMNET_API_KEY tanımlı değil. Panel → Ayarlar → API Anahtarları bölümünden key oluşturun.');
|
|
13
|
+
return {
|
|
14
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const agentTools = [
|
|
20
|
+
{
|
|
21
|
+
name: 'get_my_leads',
|
|
22
|
+
description: `Emlakçının lead (başvuru) listesini getir.
|
|
23
|
+
Filtreleme: durum (NEW=Yeni, CONTACTED=Arandı, MEETING=Görüşme, CLOSED=Kapandı).
|
|
24
|
+
Örnek: "Bu hafta kaç yeni lead geldi?", "Aranmamış başvurularım hangileri?"`,
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
status: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
enum: ['NEW', 'CONTACTED', 'MEETING', 'CLOSED'],
|
|
31
|
+
description: 'Durum filtresi (boş = hepsi)',
|
|
32
|
+
},
|
|
33
|
+
page: { type: 'number', description: 'Sayfa numarası (varsayılan: 1)' },
|
|
34
|
+
limit: { type: 'number', description: 'Sayfa başı kayıt (maks: 50, varsayılan: 20)' },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
name: 'update_lead_status',
|
|
41
|
+
description: `Lead durumunu güncelle.
|
|
42
|
+
Örnek: "Ahmet Yılmaz'ı aradım, durumu 'Arandı' yap"`,
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
leadId: { type: 'string', description: 'Lead ID (get_my_leads\'ten alınır)' },
|
|
47
|
+
status: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
enum: ['NEW', 'CONTACTED', 'MEETING', 'CLOSED'],
|
|
50
|
+
description: 'Yeni durum',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
required: ['leadId', 'status'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
name: 'get_my_listings',
|
|
59
|
+
description: `Emlakçının ilanlarını getir.
|
|
60
|
+
Aktif, taslak, pasif ilanlar. Lead sayısı ve görüntülenme bilgisi dahil.
|
|
61
|
+
Örnek: "Aktif ilanlarımı listele", "Taslak ilanlarım var mı?"`,
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
status: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
enum: ['DRAFT', 'PENDING', 'APPROVED', 'PASSIVE', 'SOLD', 'RENTED', 'REJECTED'],
|
|
68
|
+
description: 'Durum filtresi (boş = hepsi)',
|
|
69
|
+
},
|
|
70
|
+
page: { type: 'number', description: 'Sayfa numarası' },
|
|
71
|
+
limit: { type: 'number', description: 'Sayfa başı kayıt' },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
{
|
|
77
|
+
name: 'get_my_stats',
|
|
78
|
+
description: `Dashboard özeti: aktif ilan sayısı, yeni lead sayısı, toplam görüntülenme.
|
|
79
|
+
Örnek: "Durumumu özetle", "Bu hafta kaç kişi ilanlarımı gördü?"`,
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
export async function handleAgentTool(name, args) {
|
|
88
|
+
if (name === 'get_my_leads') return getMyLeads(args);
|
|
89
|
+
if (name === 'update_lead_status') return updateLeadStatus(args);
|
|
90
|
+
if (name === 'get_my_listings') return getMyListings(args);
|
|
91
|
+
if (name === 'get_my_stats') return getMyStats();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function getMyLeads({ status, page = 1, limit = 20 } = {}) {
|
|
95
|
+
const params = new URLSearchParams({ page: String(page), limit: String(limit) });
|
|
96
|
+
if (status) params.set('status', status);
|
|
97
|
+
|
|
98
|
+
const res = await fetch(`${BASE_URL}/api/mcp/leads?${params}`, { headers: agentHeaders() });
|
|
99
|
+
if (!res.ok) throw new Error(`API hatası: ${res.status}`);
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
|
|
102
|
+
const leads = data.leads.map(l => ({
|
|
103
|
+
id: l.id,
|
|
104
|
+
name: l.name,
|
|
105
|
+
email: l.email,
|
|
106
|
+
phone: l.phone || null,
|
|
107
|
+
status: l.status,
|
|
108
|
+
message: l.message || null,
|
|
109
|
+
listing: l.listing ? { title: l.listing.title, slug: l.listing.slug, price: `${l.listing.priceTRY?.toLocaleString('tr-TR')} ₺` } : null,
|
|
110
|
+
createdAt: l.createdAt,
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
content: [{
|
|
115
|
+
type: 'text',
|
|
116
|
+
text: JSON.stringify({ leads, total: data.total, page: data.page }, null, 2),
|
|
117
|
+
}],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function updateLeadStatus({ leadId, status }) {
|
|
122
|
+
const res = await fetch(`${BASE_URL}/api/mcp/leads/${leadId}`, {
|
|
123
|
+
method: 'PATCH',
|
|
124
|
+
headers: agentHeaders(),
|
|
125
|
+
body: JSON.stringify({ status }),
|
|
126
|
+
});
|
|
127
|
+
if (!res.ok) throw new Error(`API hatası: ${res.status}`);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
content: [{
|
|
131
|
+
type: 'text',
|
|
132
|
+
text: JSON.stringify({ success: true, leadId, status }, null, 2),
|
|
133
|
+
}],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function getMyListings({ status, page = 1, limit = 20 } = {}) {
|
|
138
|
+
const params = new URLSearchParams({ page: String(page), limit: String(limit) });
|
|
139
|
+
if (status) params.set('status', status);
|
|
140
|
+
|
|
141
|
+
const res = await fetch(`${BASE_URL}/api/mcp/listings?${params}`, { headers: agentHeaders() });
|
|
142
|
+
if (!res.ok) throw new Error(`API hatası: ${res.status}`);
|
|
143
|
+
const data = await res.json();
|
|
144
|
+
|
|
145
|
+
const listings = data.listings.map(l => ({
|
|
146
|
+
id: l.id,
|
|
147
|
+
title: l.title,
|
|
148
|
+
slug: l.slug,
|
|
149
|
+
type: l.type,
|
|
150
|
+
status: l.status,
|
|
151
|
+
price: `${l.priceTRY?.toLocaleString('tr-TR')} ₺`,
|
|
152
|
+
sqm: l.sqmNet ? `${l.sqmNet}m²` : null,
|
|
153
|
+
rooms: l.rooms || null,
|
|
154
|
+
city: l.city?.name || null,
|
|
155
|
+
district: l.district?.name || null,
|
|
156
|
+
leads: l._count?.leads || 0,
|
|
157
|
+
url: `https://evimnet.com/ilan/${l.slug}`,
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
content: [{
|
|
162
|
+
type: 'text',
|
|
163
|
+
text: JSON.stringify({ listings, total: data.total }, null, 2),
|
|
164
|
+
}],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function getMyStats() {
|
|
169
|
+
const res = await fetch(`${BASE_URL}/api/mcp/stats`, { headers: agentHeaders() });
|
|
170
|
+
if (!res.ok) throw new Error(`API hatası: ${res.status}`);
|
|
171
|
+
const data = await res.json();
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
content: [{
|
|
175
|
+
type: 'text',
|
|
176
|
+
text: JSON.stringify(data, null, 2),
|
|
177
|
+
}],
|
|
178
|
+
};
|
|
179
|
+
}
|
package/tools/lead.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lead Tool — Emlakçıya mesaj / başvuru gönder
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const BASE_URL = process.env.EVIMNET_BASE_URL || 'https://evimnet.com';
|
|
6
|
+
|
|
7
|
+
export const leadTools = [
|
|
8
|
+
{
|
|
9
|
+
name: 'send_inquiry',
|
|
10
|
+
description: `Bir ilan için emlakçıya başvuru/mesaj gönder.
|
|
11
|
+
Kullanıcının ilana ilgi duyduğunu emlakçıya ilet.
|
|
12
|
+
İlan slug'ı ve emlakçı bilgileri get_property_details'ten alınabilir.
|
|
13
|
+
Rate limit: 5 başvuru / 5 dakika.`,
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
listingSlug: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'İlan slug\'ı (get_property_details\'ten alınır)',
|
|
20
|
+
},
|
|
21
|
+
agentId: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'Emlakçı ID\'si (get_property_details\'ten alınır)',
|
|
24
|
+
},
|
|
25
|
+
name: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'Başvuru sahibinin adı soyadı',
|
|
28
|
+
},
|
|
29
|
+
email: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'E-posta adresi',
|
|
32
|
+
},
|
|
33
|
+
phone: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Telefon numarası (opsiyonel)',
|
|
36
|
+
},
|
|
37
|
+
message: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'Mesaj içeriği',
|
|
40
|
+
},
|
|
41
|
+
availability: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Uygun zaman (örn: "Hafta içi öğleden sonra", "Cumartesi")',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
required: ['listingSlug', 'agentId', 'name', 'email'],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export async function handleLeadTool(name, args) {
|
|
52
|
+
if (name === 'send_inquiry') return sendInquiry(args);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function sendInquiry(args) {
|
|
56
|
+
const { listingSlug, agentId, name, email, phone, message, availability } = args;
|
|
57
|
+
|
|
58
|
+
// İlan ID'sini slug'dan bul
|
|
59
|
+
const listingRes = await fetch(`${BASE_URL}/api/listings/${listingSlug}`);
|
|
60
|
+
if (!listingRes.ok) throw new Error(listingRes.status === 404 ? 'İlan bulunamadı' : `API hatası: ${listingRes.status}`);
|
|
61
|
+
const { data: listing } = await listingRes.json();
|
|
62
|
+
|
|
63
|
+
const body = {
|
|
64
|
+
listingId: listing.id,
|
|
65
|
+
agentId,
|
|
66
|
+
name,
|
|
67
|
+
email,
|
|
68
|
+
phone: phone || undefined,
|
|
69
|
+
message: message || `${listing.title} ilanı hakkında bilgi almak istiyorum.`,
|
|
70
|
+
availability: availability || undefined,
|
|
71
|
+
source: 'MCP',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const res = await fetch(`${BASE_URL}/api/leads`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'Content-Type': 'application/json' },
|
|
77
|
+
body: JSON.stringify(body),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (res.status === 429) throw new Error('Çok fazla başvuru gönderildi. Lütfen birkaç dakika bekleyin.');
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
const err = await res.json().catch(() => ({}));
|
|
83
|
+
throw new Error(err.error || `Başvuru gönderilemedi: ${res.status}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const lead = await res.json();
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
content: [{
|
|
90
|
+
type: 'text',
|
|
91
|
+
text: JSON.stringify({
|
|
92
|
+
success: true,
|
|
93
|
+
message: 'Başvurunuz emlakçıya iletildi. En kısa sürede sizinle iletişime geçecekler.',
|
|
94
|
+
leadId: lead.id,
|
|
95
|
+
listing: listing.title,
|
|
96
|
+
agent: listing.agent?.name || null,
|
|
97
|
+
sentAt: new Date().toISOString(),
|
|
98
|
+
}, null, 2),
|
|
99
|
+
}],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Listings Tools — İlan arama ve detay araçları
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const BASE_URL = process.env.EVIMNET_BASE_URL || 'https://evimnet.com';
|
|
6
|
+
|
|
7
|
+
export const listingTools = [
|
|
8
|
+
{
|
|
9
|
+
name: 'search_properties',
|
|
10
|
+
description: `Evimnet'te gayrimenkul ara. Türkiye'deki satılık/kiralık daire, villa, arsa ve projeleri filtrele.
|
|
11
|
+
Örnekler: "Bodrum'da deniz manzaralı satılık villa", "İstanbul Beşiktaş'ta 2+1 kiralık daire", "Antalya'da arsa"`,
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
query: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'Serbest metin arama (opsiyonel)',
|
|
18
|
+
},
|
|
19
|
+
type: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
enum: ['SALE', 'RENT', 'PROJECT_SALE'],
|
|
22
|
+
description: 'İlan tipi: SALE=Satılık, RENT=Kiralık, PROJECT_SALE=Proje',
|
|
23
|
+
},
|
|
24
|
+
propertyType: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
enum: ['APARTMENT', 'VILLA', 'LAND', 'PROJECT'],
|
|
27
|
+
description: 'Mülk tipi: APARTMENT=Daire, VILLA=Villa, LAND=Arsa, PROJECT=Proje',
|
|
28
|
+
},
|
|
29
|
+
city: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Şehir slug (örn: istanbul, bodrum, antalya, izmir)',
|
|
32
|
+
},
|
|
33
|
+
district: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'İlçe slug (örn: besiktas, marmaris, kemer)',
|
|
36
|
+
},
|
|
37
|
+
neighborhood: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'Mahalle slug',
|
|
40
|
+
},
|
|
41
|
+
minPrice: {
|
|
42
|
+
type: 'number',
|
|
43
|
+
description: 'Minimum fiyat (TRY)',
|
|
44
|
+
},
|
|
45
|
+
maxPrice: {
|
|
46
|
+
type: 'number',
|
|
47
|
+
description: 'Maksimum fiyat (TRY)',
|
|
48
|
+
},
|
|
49
|
+
minSqm: {
|
|
50
|
+
type: 'number',
|
|
51
|
+
description: 'Minimum metrekare (brüt)',
|
|
52
|
+
},
|
|
53
|
+
maxSqm: {
|
|
54
|
+
type: 'number',
|
|
55
|
+
description: 'Maksimum metrekare (brüt)',
|
|
56
|
+
},
|
|
57
|
+
rooms: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'Oda sayısı (örn: "1+1", "2+1", "3+1", "Stüdyo")',
|
|
60
|
+
},
|
|
61
|
+
viewType: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
enum: ['SEA', 'CITY', 'NATURE', 'LAKE', 'MOUNTAIN'],
|
|
64
|
+
description: 'Manzara tipi',
|
|
65
|
+
},
|
|
66
|
+
villaType: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
enum: ['DETACHED', 'SEMI_DETACHED', 'TRIPLEX', 'DUPLEX'],
|
|
69
|
+
description: 'Villa tipi',
|
|
70
|
+
},
|
|
71
|
+
poolType: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
enum: ['PRIVATE', 'SHARED', 'NONE'],
|
|
74
|
+
description: 'Havuz tipi',
|
|
75
|
+
},
|
|
76
|
+
sort: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
enum: ['newest', 'oldest', 'price_asc', 'price_desc'],
|
|
79
|
+
description: 'Sıralama (varsayılan: newest)',
|
|
80
|
+
},
|
|
81
|
+
page: {
|
|
82
|
+
type: 'number',
|
|
83
|
+
description: 'Sayfa numarası (varsayılan: 1)',
|
|
84
|
+
},
|
|
85
|
+
limit: {
|
|
86
|
+
type: 'number',
|
|
87
|
+
description: 'Sayfa başına sonuç (varsayılan: 10, max: 50)',
|
|
88
|
+
},
|
|
89
|
+
lang: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
enum: ['tr', 'de'],
|
|
92
|
+
description: 'Dil (varsayılan: tr)',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
{
|
|
99
|
+
name: 'get_property_details',
|
|
100
|
+
description: `Bir gayrimenkulün tüm detaylarını getir: fiyat, konum, özellikler, fotoğraflar, emlakçı bilgileri.
|
|
101
|
+
Slug veya tam URL kabul eder.`,
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
slug: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: 'İlan slug\'ı (örn: bodrum-satilik-villa-123) veya tam URL',
|
|
108
|
+
},
|
|
109
|
+
lang: {
|
|
110
|
+
type: 'string',
|
|
111
|
+
enum: ['tr', 'de'],
|
|
112
|
+
description: 'Dil (varsayılan: tr)',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
required: ['slug'],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
export async function handleListingsTool(name, args) {
|
|
121
|
+
if (name === 'search_properties') {
|
|
122
|
+
return searchProperties(args);
|
|
123
|
+
}
|
|
124
|
+
if (name === 'get_property_details') {
|
|
125
|
+
return getPropertyDetails(args);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function searchProperties(args) {
|
|
130
|
+
const params = new URLSearchParams();
|
|
131
|
+
|
|
132
|
+
if (args.query) params.set('q', args.query);
|
|
133
|
+
if (args.type) params.set('type', args.type);
|
|
134
|
+
if (args.propertyType) params.set('propertyType', args.propertyType);
|
|
135
|
+
if (args.city) params.set('city', args.city);
|
|
136
|
+
if (args.district) params.set('district', args.district);
|
|
137
|
+
if (args.neighborhood) params.set('neighborhood', args.neighborhood);
|
|
138
|
+
if (args.minPrice) params.set('minPrice', args.minPrice);
|
|
139
|
+
if (args.maxPrice) params.set('maxPrice', args.maxPrice);
|
|
140
|
+
if (args.minSqm) params.set('minSqm', args.minSqm);
|
|
141
|
+
if (args.maxSqm) params.set('maxSqm', args.maxSqm);
|
|
142
|
+
if (args.rooms) params.set('rooms', args.rooms);
|
|
143
|
+
if (args.viewType) params.set('viewType', args.viewType);
|
|
144
|
+
if (args.villaType) params.set('villaType', args.villaType);
|
|
145
|
+
if (args.poolType) params.set('poolType', args.poolType);
|
|
146
|
+
if (args.sort) params.set('sort', args.sort);
|
|
147
|
+
if (args.lang) params.set('lang', args.lang);
|
|
148
|
+
|
|
149
|
+
params.set('page', String(args.page || 1));
|
|
150
|
+
params.set('limit', String(Math.min(args.limit || 10, 50)));
|
|
151
|
+
|
|
152
|
+
const res = await fetch(`${BASE_URL}/api/listings?${params}`);
|
|
153
|
+
if (!res.ok) throw new Error(`API hatası: ${res.status}`);
|
|
154
|
+
const json = await res.json();
|
|
155
|
+
|
|
156
|
+
const listings = (json.data || []).map(l => ({
|
|
157
|
+
slug: l.slug,
|
|
158
|
+
title: l.title,
|
|
159
|
+
type: l.type === 'SALE' ? 'Satılık' : l.type === 'RENT' ? 'Kiralık' : 'Proje',
|
|
160
|
+
propertyType: formatPropertyType(l.propertyType),
|
|
161
|
+
price: `${l.price?.try?.toLocaleString('tr-TR')} ₺`,
|
|
162
|
+
area: l.area?.gross ? `${l.area.gross} m²` : null,
|
|
163
|
+
rooms: l.rooms || null,
|
|
164
|
+
floor: l.floor != null ? `${l.floor}. kat` : null,
|
|
165
|
+
view: l.viewType?.length ? l.viewType.join(', ') : null,
|
|
166
|
+
location: [l.location?.neighborhood, l.location?.district, l.location?.city].filter(Boolean).join(', '),
|
|
167
|
+
agent: l.agent?.name || null,
|
|
168
|
+
office: l.agent?.company || null,
|
|
169
|
+
image: l.image || null,
|
|
170
|
+
url: `${BASE_URL}/tr/ilanlar/${l.slug}`,
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
content: [{
|
|
175
|
+
type: 'text',
|
|
176
|
+
text: JSON.stringify({
|
|
177
|
+
results: listings,
|
|
178
|
+
total: json.pagination?.total || listings.length,
|
|
179
|
+
page: json.pagination?.page || 1,
|
|
180
|
+
totalPages: json.pagination?.totalPages || 1,
|
|
181
|
+
hasNext: json.pagination?.hasNext || false,
|
|
182
|
+
}, null, 2),
|
|
183
|
+
}],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function getPropertyDetails(args) {
|
|
188
|
+
// Slug'dan URL parçasını çıkar
|
|
189
|
+
let slug = args.slug
|
|
190
|
+
.replace(/^https?:\/\/[^/]+/, '')
|
|
191
|
+
.replace(/^\/[a-z]{2}\/ilanlar\//, '')
|
|
192
|
+
.replace(/^\//, '');
|
|
193
|
+
|
|
194
|
+
const lang = args.lang || 'tr';
|
|
195
|
+
const res = await fetch(`${BASE_URL}/api/listings/${slug}?lang=${lang}`);
|
|
196
|
+
if (!res.ok) throw new Error(res.status === 404 ? 'İlan bulunamadı' : `API hatası: ${res.status}`);
|
|
197
|
+
|
|
198
|
+
const { data: l } = await res.json();
|
|
199
|
+
|
|
200
|
+
const detail = {
|
|
201
|
+
slug: l.slug,
|
|
202
|
+
title: l.title,
|
|
203
|
+
description: l.description?.slice(0, 800) || null,
|
|
204
|
+
type: l.type === 'SALE' ? 'Satılık' : l.type === 'RENT' ? 'Kiralık' : 'Proje',
|
|
205
|
+
propertyType: formatPropertyType(l.propertyType),
|
|
206
|
+
status: l.status,
|
|
207
|
+
price: {
|
|
208
|
+
try: `${l.price?.try?.toLocaleString('tr-TR')} ₺`,
|
|
209
|
+
dues: l.price?.dues ? `${l.price.dues} ₺/ay aidat` : null,
|
|
210
|
+
},
|
|
211
|
+
area: {
|
|
212
|
+
gross: l.area?.gross ? `${l.area.gross} m² brüt` : null,
|
|
213
|
+
net: l.area?.net ? `${l.area.net} m² net` : null,
|
|
214
|
+
},
|
|
215
|
+
details: {
|
|
216
|
+
rooms: l.rooms || null,
|
|
217
|
+
bathrooms: l.bathrooms ? `${l.bathrooms} banyo` : null,
|
|
218
|
+
floor: l.floor != null ? `${l.floor}. kat / ${l.totalFloors || '?'} katlı` : null,
|
|
219
|
+
buildingAge: l.buildingAge ? `${l.buildingAge} yaşında` : null,
|
|
220
|
+
heating: l.heating || null,
|
|
221
|
+
deedStatus: l.deedStatus || null,
|
|
222
|
+
},
|
|
223
|
+
features: {
|
|
224
|
+
view: l.viewType?.length ? l.viewType.join(', ') : null,
|
|
225
|
+
seaDistance: l.seaDistance ? `Denize ${l.seaDistance}m` : null,
|
|
226
|
+
villaType: l.villaType || null,
|
|
227
|
+
poolType: l.poolType || null,
|
|
228
|
+
inComplex: l.isInComplex ? `Sitede (${l.complexName || ''})` : null,
|
|
229
|
+
},
|
|
230
|
+
location: {
|
|
231
|
+
city: l.location?.city || null,
|
|
232
|
+
district: l.location?.district || null,
|
|
233
|
+
neighborhood: l.location?.neighborhood || null,
|
|
234
|
+
coordinates: l.location?.lat ? { lat: l.location.lat, lng: l.location.lng } : null,
|
|
235
|
+
},
|
|
236
|
+
agent: {
|
|
237
|
+
name: l.agent?.name || null,
|
|
238
|
+
company: l.agent?.company || null,
|
|
239
|
+
office: l.agent?.office || null,
|
|
240
|
+
phone: l.agent?.phone || null,
|
|
241
|
+
email: l.agent?.email || null,
|
|
242
|
+
},
|
|
243
|
+
images: (l.images || []).slice(0, 5).map(i => i.url),
|
|
244
|
+
stats: {
|
|
245
|
+
totalViews: l.viewsTotal || 0,
|
|
246
|
+
europeViews: l.europeViews || 0,
|
|
247
|
+
},
|
|
248
|
+
url: `${BASE_URL}/tr/ilanlar/${l.slug}`,
|
|
249
|
+
createdAt: l.createdAt?.slice(0, 10) || null,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
content: [{
|
|
254
|
+
type: 'text',
|
|
255
|
+
text: JSON.stringify(detail, null, 2),
|
|
256
|
+
}],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function formatPropertyType(type) {
|
|
261
|
+
const map = { APARTMENT: 'Daire', VILLA: 'Villa', LAND: 'Arsa', PROJECT: 'Proje' };
|
|
262
|
+
return map[type] || type;
|
|
263
|
+
}
|