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/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
+ }