'object', 'properties' => $props]; if ($required) $schema['required'] = $required; return ['name' => $name, 'description' => $desc, 'scope' => $scope, 'handler' => 'AskTamMCP\\Extensions\\Batch8::' . $method, 'inputSchema' => $schema]; }; $registry->register($def('remote_fetch_json', 'Fetch a JSON URL (GET) with transient caching. Optional dot-path (e.g. "data.0.name") extracts part of the response. cache_minutes default 15.', ['url' => ['type' => 'string'], 'cache_minutes' => ['type' => 'integer', 'default' => 15], 'path' => ['type' => 'string'], 'headers' => ['type' => 'object']], ['url'], 'read', 'fetchJson')); $registry->register($def('remote_weather', 'Current weather from OpenWeather. Provide api_key and either city or lat+lon. units=metric (default) or imperial. Cached 30 min.', ['api_key' => ['type' => 'string'], 'city' => ['type' => 'string'], 'lat' => ['type' => 'number'], 'lon' => ['type' => 'number'], 'units' => ['type' => 'string', 'enum' => ['metric', 'imperial'], 'default' => 'metric']], ['api_key'], 'read', 'weather')); $registry->register($def('remote_place_reviews', 'Google Place rating + reviews via Places Details API. Provide api_key and place_id. Cached 60 min.', ['api_key' => ['type' => 'string'], 'place_id' => ['type' => 'string']], ['api_key', 'place_id'], 'read', 'placeReviews')); $registry->register($def('remote_list_cache', 'List cached remote-data entries (key + size).', [], [], 'read', 'listCache')); $registry->register($def('remote_clear_cache', 'Clear cached remote-data. Provide key to clear one, or omit to clear all remote-data cache.', ['key' => ['type' => 'string']], [], 'write', 'clearCache')); } private static function cacheKey(string $raw): string { return self::PREFIX . md5($raw); } private static function path($data, string $path) { if ($path === '') return $data; foreach (explode('.', $path) as $seg) { if (is_array($data) && array_key_exists($seg, $data)) $data = $data[$seg]; elseif (is_array($data) && ctype_digit($seg) && array_key_exists((int) $seg, $data)) $data = $data[(int) $seg]; else return null; } return $data; } private static function getJson(string $url, int $cacheMin, array $headers = []) { $key = self::cacheKey($url . serialize($headers)); $cached = get_transient($key); if ($cached !== false) return ['data' => $cached, 'cached' => true]; $args = ['timeout' => 15]; if ($headers) $args['headers'] = $headers; $resp = wp_remote_get($url, $args); if (is_wp_error($resp)) throw new \RuntimeException('Fetch failed: ' . $resp->get_error_message()); $code = (int) wp_remote_retrieve_response_code($resp); $body = wp_remote_retrieve_body($resp); $json = json_decode($body, true); if ($json === null && trim($body) !== 'null') throw new \RuntimeException("Non-JSON response (HTTP {$code})."); if ($cacheMin > 0) set_transient($key, $json, $cacheMin * MINUTE_IN_SECONDS); return ['data' => $json, 'cached' => false, 'http_code' => $code]; } public static function fetchJson(array $args): array { $url = (string) ($args['url'] ?? ''); if (!filter_var($url, FILTER_VALIDATE_URL)) throw new \RuntimeException('Valid url is required.'); $min = (int) ($args['cache_minutes'] ?? 15); $headers = is_array($args['headers'] ?? null) ? $args['headers'] : []; $res = self::getJson($url, $min, $headers); $data = isset($args['path']) ? self::path($res['data'], (string) $args['path']) : $res['data']; return ['url' => $url, 'cached' => $res['cached'], 'data' => $data]; } public static function weather(array $args): array { $key = (string) ($args['api_key'] ?? ''); if ($key === '') throw new \RuntimeException('api_key is required.'); $units = (($args['units'] ?? 'metric') === 'imperial') ? 'imperial' : 'metric'; $q = ['appid' => $key, 'units' => $units]; if (!empty($args['city'])) $q['q'] = (string) $args['city']; elseif (isset($args['lat'], $args['lon'])) { $q['lat'] = (float) $args['lat']; $q['lon'] = (float) $args['lon']; } else throw new \RuntimeException('Provide city or lat+lon.'); $url = 'https://api.openweathermap.org/data/2.5/weather?' . http_build_query($q); $res = self::getJson($url, (int) ($args['cache_minutes'] ?? 30)); $d = $res['data']; return [ 'cached' => $res['cached'], 'location' => $d['name'] ?? '', 'temp' => $d['main']['temp'] ?? null, 'feels_like' => $d['main']['feels_like'] ?? null, 'humidity' => $d['main']['humidity'] ?? null, 'conditions' => $d['weather'][0]['description'] ?? '', 'units' => $units, ]; } public static function placeReviews(array $args): array { $key = (string) ($args['api_key'] ?? ''); $pid = (string) ($args['place_id'] ?? ''); if ($key === '' || $pid === '') throw new \RuntimeException('api_key and place_id are required.'); $url = 'https://maps.googleapis.com/maps/api/place/details/json?' . http_build_query([ 'place_id' => $pid, 'key' => $key, 'fields' => 'name,rating,user_ratings_total,reviews', ]); $res = self::getJson($url, (int) ($args['cache_minutes'] ?? 60)); $r = $res['data']['result'] ?? []; $reviews = array_map(fn($rv) => [ 'author' => $rv['author_name'] ?? '', 'rating' => $rv['rating'] ?? null, 'text' => $rv['text'] ?? '', 'time' => $rv['relative_time_description'] ?? '', ], (array) ($r['reviews'] ?? [])); return ['cached' => $res['cached'], 'name' => $r['name'] ?? '', 'rating' => $r['rating'] ?? null, 'total_ratings' => $r['user_ratings_total'] ?? null, 'reviews' => $reviews]; } public static function listCache(array $args): array { global $wpdb; $like = '_transient_' . self::PREFIX . '%'; $rows = $wpdb->get_results($wpdb->prepare("SELECT option_name, LENGTH(option_value) AS len FROM {$wpdb->options} WHERE option_name LIKE %s", $like)); $out = []; foreach ((array) $rows as $row) { $out[] = ['key' => str_replace('_transient_', '', $row->option_name), 'bytes' => (int) $row->len]; } return ['count' => count($out), 'entries' => $out]; } public static function clearCache(array $args): array { global $wpdb; if (!empty($args['key'])) { $k = (string) $args['key']; $ok = delete_transient($k); return ['cleared' => $ok ? 1 : 0, 'key' => $k]; } $like = '_transient_' . self::PREFIX . '%'; $names = $wpdb->get_col($wpdb->prepare("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $like)); $n = 0; foreach ((array) $names as $name) { if (delete_transient(str_replace('_transient_', '', $name))) $n++; } return ['cleared' => $n]; } }