@tikomni/skills 1.0.5 → 1.0.7
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 +3 -3
- package/README.zh-CN.md +3 -3
- package/env.example +2 -2
- package/package.json +2 -2
- package/skills/social-media-crawl/SKILL.md +20 -16
- package/skills/social-media-crawl/agents/openai.yaml +2 -2
- package/skills/social-media-crawl/references/api-catalog/aliases.json +237 -0
- package/skills/social-media-crawl/references/api-catalog/capabilities.json +362 -0
- package/skills/social-media-crawl/references/api-catalog/metadata.json +11 -0
- package/skills/social-media-crawl/references/api-catalog/operations.json +100875 -0
- package/skills/social-media-crawl/references/api-catalog/overrides.json +10 -0
- package/skills/social-media-crawl/references/api-catalog/platforms.json +463 -0
- package/skills/social-media-crawl/references/api-routing-contract.md +73 -0
- package/skills/social-media-crawl/references/contracts/output-envelope.md +1 -1
- package/skills/social-media-crawl/scripts/core/api_catalog.py +104 -0
- package/skills/social-media-crawl/scripts/core/call_tikomni_api.py +269 -0
- package/skills/social-media-crawl/scripts/core/config_loader.py +0 -2
- package/skills/social-media-crawl/scripts/core/resolve_api_endpoint.py +176 -0
- package/skills/social-media-crawl/scripts/pipelines/run_douyin_single_work.py +16 -17
- package/skills/social-media-crawl/scripts/pipelines/run_xiaohongshu_single_work.py +16 -16
- package/skills/social-media-crawl/scripts/run_task.py +96 -0
- package/skills/social-media-crawl/tests/test_api_only_routing.py +130 -0
- package/skills/social-media-crawl/tests/test_fixed_pipeline_fallback.py +50 -0
- package/skills/social-media-crawl/references/guides/generic-mcp-objects.md +0 -40
- package/skills/social-media-crawl/references/mcp-usage-contract.md +0 -40
- package/skills/social-media-crawl/scripts/core/mcp_dispatch.py +0 -161
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import unittest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
|
11
|
+
if str(SKILL_ROOT) not in sys.path:
|
|
12
|
+
sys.path.insert(0, str(SKILL_ROOT))
|
|
13
|
+
|
|
14
|
+
from scripts.core.call_tikomni_api import call_endpoint
|
|
15
|
+
from scripts.core.resolve_api_endpoint import resolve_endpoint
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ApiOnlyRoutingTest(unittest.TestCase):
|
|
19
|
+
@classmethod
|
|
20
|
+
def setUpClass(cls) -> None:
|
|
21
|
+
cls.operations = json.loads(
|
|
22
|
+
(SKILL_ROOT / "references" / "api-catalog" / "operations.json").read_text(encoding="utf-8")
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def test_error_report_paths_are_not_in_catalog(self) -> None:
|
|
26
|
+
paths = {str(operation["path"]) for operation in self.operations}
|
|
27
|
+
wrong_paths = {
|
|
28
|
+
"/api/u1/v1/health",
|
|
29
|
+
"/api/u1/v1/douyin/hot_search_list",
|
|
30
|
+
"/api/u1/v1/Douyin/douyin_hot_search_list",
|
|
31
|
+
"/api/u1/v1/wechat/article",
|
|
32
|
+
"/api/u1/v1/wechat/fetch_article",
|
|
33
|
+
"/api/u1/v1/account/info",
|
|
34
|
+
"/api/u1/v1/quote/list",
|
|
35
|
+
"/api/u1/v1/demo/wechat/article",
|
|
36
|
+
}
|
|
37
|
+
self.assertTrue(wrong_paths.isdisjoint(paths))
|
|
38
|
+
self.assertIn("/api/u1/v1/demo/douyin/web/fetch_one_video", paths)
|
|
39
|
+
|
|
40
|
+
def test_douyin_hot_search_resolves_real_endpoint(self) -> None:
|
|
41
|
+
resolved = resolve_endpoint(platform="douyin", capability="hot_search")
|
|
42
|
+
self.assertTrue(resolved["ok"])
|
|
43
|
+
self.assertEqual(
|
|
44
|
+
resolved["recommended"]["endpoint_id"],
|
|
45
|
+
"douyin.hot_search.app_v3.fetch_hot_search_list",
|
|
46
|
+
)
|
|
47
|
+
self.assertEqual(
|
|
48
|
+
resolved["recommended"]["path"],
|
|
49
|
+
"/api/u1/v1/douyin/app/v3/fetch_hot_search_list",
|
|
50
|
+
)
|
|
51
|
+
self.assertTrue(resolved["alternatives"])
|
|
52
|
+
|
|
53
|
+
def test_wechat_mp_and_channels_are_split(self) -> None:
|
|
54
|
+
mp = resolve_endpoint(platform="wechat_mp", capability="article_detail")
|
|
55
|
+
self.assertTrue(mp["ok"])
|
|
56
|
+
self.assertEqual(mp["platform"], "wechat_mp")
|
|
57
|
+
self.assertEqual(mp["recommended"]["path"], "/api/u1/v1/wechat_mp/web/fetch_mp_article_detail_json")
|
|
58
|
+
|
|
59
|
+
channels = resolve_endpoint(platform="wechat_channels", capability="video_detail")
|
|
60
|
+
self.assertTrue(channels["ok"])
|
|
61
|
+
self.assertEqual(channels["platform"], "wechat_channels")
|
|
62
|
+
self.assertEqual(channels["recommended"]["path"], "/api/u1/v1/wechat_channels/fetch_video_detail")
|
|
63
|
+
|
|
64
|
+
def test_ambiguous_wechat_returns_suggestions_not_guess(self) -> None:
|
|
65
|
+
resolved = resolve_endpoint(platform="wechat", capability="article_detail")
|
|
66
|
+
self.assertFalse(resolved["ok"])
|
|
67
|
+
self.assertEqual(resolved["code"], "ENDPOINT_NOT_RESOLVED")
|
|
68
|
+
self.assertIn({"platform": "wechat_mp", "capability": "article_detail"}, resolved["suggestions"])
|
|
69
|
+
|
|
70
|
+
def test_caller_rejects_arbitrary_routing_params(self) -> None:
|
|
71
|
+
result = call_endpoint(
|
|
72
|
+
endpoint_id="douyin.hot_search.app_v3.fetch_hot_search_list",
|
|
73
|
+
params={"path": "/api/u1/v1/douyin/hot_search_list"},
|
|
74
|
+
dry_run=True,
|
|
75
|
+
)
|
|
76
|
+
self.assertFalse(result["ok"])
|
|
77
|
+
self.assertEqual(result["code"], "RESERVED_PARAM_KEY")
|
|
78
|
+
|
|
79
|
+
result = call_endpoint(
|
|
80
|
+
endpoint_id="douyin.hot_search.app_v3.fetch_hot_search_list",
|
|
81
|
+
params={"method": "POST"},
|
|
82
|
+
dry_run=True,
|
|
83
|
+
)
|
|
84
|
+
self.assertFalse(result["ok"])
|
|
85
|
+
self.assertEqual(result["code"], "RESERVED_PARAM_KEY")
|
|
86
|
+
|
|
87
|
+
def test_caller_allows_declared_path_payload_field(self) -> None:
|
|
88
|
+
result = call_endpoint(
|
|
89
|
+
endpoint_id="xiaohongshu.search.web.sign",
|
|
90
|
+
params={
|
|
91
|
+
"path": "/api/sns/web/v1/search/notes",
|
|
92
|
+
"cookie": "web_session=test",
|
|
93
|
+
"data": {"keyword": "test"},
|
|
94
|
+
},
|
|
95
|
+
dry_run=True,
|
|
96
|
+
)
|
|
97
|
+
self.assertTrue(result["ok"])
|
|
98
|
+
self.assertEqual(result["request"]["path"], "/api/u1/v1/xiaohongshu/web/sign")
|
|
99
|
+
self.assertEqual(result["request"]["body"]["path"], "/api/sns/web/v1/search/notes")
|
|
100
|
+
|
|
101
|
+
def test_caller_rejects_unknown_endpoint_id(self) -> None:
|
|
102
|
+
result = call_endpoint(
|
|
103
|
+
endpoint_id="douyin.hot_search.app_v3.not_real",
|
|
104
|
+
params={},
|
|
105
|
+
dry_run=True,
|
|
106
|
+
)
|
|
107
|
+
self.assertFalse(result["ok"])
|
|
108
|
+
self.assertEqual(result["code"], "UNKNOWN_ENDPOINT_ID")
|
|
109
|
+
|
|
110
|
+
def test_u2_u3_stay_in_same_catalog(self) -> None:
|
|
111
|
+
endpoint_ids = {str(operation["endpoint_id"]) for operation in self.operations}
|
|
112
|
+
self.assertIn("u2.transcription.service.transcription", endpoint_ids)
|
|
113
|
+
self.assertIn("u3.media_upload.service.uploads", endpoint_ids)
|
|
114
|
+
|
|
115
|
+
def test_plain_english_intent_does_not_match_single_letter_x_alias(self) -> None:
|
|
116
|
+
resolved = resolve_endpoint(intent="extract comments from this post")
|
|
117
|
+
self.assertFalse(resolved["ok"])
|
|
118
|
+
self.assertEqual(resolved["capability"], "comments")
|
|
119
|
+
self.assertEqual(resolved["platform"], "")
|
|
120
|
+
|
|
121
|
+
def test_search_operations_are_not_classified_as_comments(self) -> None:
|
|
122
|
+
by_id = {str(operation["endpoint_id"]): operation for operation in self.operations}
|
|
123
|
+
for endpoint_id in {
|
|
124
|
+
"xiaohongshu.search.app_v2.search_notes",
|
|
125
|
+
"xiaohongshu.search.app.search_notes",
|
|
126
|
+
"xiaohongshu.search.web.search_notes",
|
|
127
|
+
"xiaohongshu.search.web.search_notes_v3",
|
|
128
|
+
}:
|
|
129
|
+
self.assertIn(endpoint_id, by_id)
|
|
130
|
+
self.assertEqual(by_id[endpoint_id]["capability"], "search")
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
import sys
|
|
7
8
|
import unittest
|
|
@@ -14,10 +15,59 @@ if str(SKILL_ROOT) not in sys.path:
|
|
|
14
15
|
|
|
15
16
|
from scripts.core.asr_pipeline import run_u2_asr_batch_with_timeout_retry
|
|
16
17
|
from scripts.pipelines import homepage_collectors
|
|
18
|
+
from scripts.pipelines import run_douyin_single_work
|
|
17
19
|
from scripts.pipelines import run_xiaohongshu_single_work
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
class FixedPipelineFallbackTest(unittest.TestCase):
|
|
23
|
+
def test_douyin_single_fetch_attempt_trace_is_json_serializable(self) -> None:
|
|
24
|
+
def fake_call_json_api(**_: object) -> dict:
|
|
25
|
+
return {
|
|
26
|
+
"ok": True,
|
|
27
|
+
"status_code": 200,
|
|
28
|
+
"request_id": "req-dy-single",
|
|
29
|
+
"error_reason": None,
|
|
30
|
+
"data": {"aweme_detail": {"aweme_id": "123"}},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
with patch.object(run_douyin_single_work, "call_json_api", side_effect=fake_call_json_api):
|
|
34
|
+
response = run_douyin_single_work._u1_fetch_one_video(
|
|
35
|
+
base_url="https://api.tikomni.com",
|
|
36
|
+
token="test-token",
|
|
37
|
+
share_url="https://v.douyin.com/test-single/",
|
|
38
|
+
app_timeout_ms=1000,
|
|
39
|
+
web_timeout_ms=1000,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
json.dumps(response, ensure_ascii=False)
|
|
43
|
+
self.assertNotIn("response", response["_attempts"][0])
|
|
44
|
+
|
|
45
|
+
def test_xhs_single_fetch_attempt_trace_is_json_serializable(self) -> None:
|
|
46
|
+
def fake_call_json_api(**_: object) -> dict:
|
|
47
|
+
return {
|
|
48
|
+
"ok": True,
|
|
49
|
+
"status_code": 200,
|
|
50
|
+
"request_id": "req-xhs-single",
|
|
51
|
+
"error_reason": None,
|
|
52
|
+
"data": {
|
|
53
|
+
"title": "test title",
|
|
54
|
+
"desc": "test content",
|
|
55
|
+
"video": {"master_url": "https://example.com/video.mp4"},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
with patch.object(run_xiaohongshu_single_work, "call_json_api", side_effect=fake_call_json_api):
|
|
60
|
+
response = run_xiaohongshu_single_work._fetch_note_info(
|
|
61
|
+
base_url="https://api.tikomni.com",
|
|
62
|
+
token="test-token",
|
|
63
|
+
timeout_ms=1000,
|
|
64
|
+
source_input={"share_text": "https://xhslink.com/test-single", "note_id": "note123"},
|
|
65
|
+
progress=None,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
json.dumps(response, ensure_ascii=False)
|
|
69
|
+
self.assertNotIn("response", response["_attempts"][0])
|
|
70
|
+
|
|
21
71
|
def test_xhs_single_route_plan_respects_version_priority_and_cookie_gate(self) -> None:
|
|
22
72
|
source_input = {
|
|
23
73
|
"share_text": (
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# Generic MCP Objects Guide
|
|
2
|
-
|
|
3
|
-
The following objects do not freeze a fine-grained schema in the first release:
|
|
4
|
-
|
|
5
|
-
- Comment threads
|
|
6
|
-
- Search results
|
|
7
|
-
- Ranking pages
|
|
8
|
-
- Livestream rooms
|
|
9
|
-
- Product pages
|
|
10
|
-
|
|
11
|
-
In addition, every platform and object combination that does not match a fixed pipeline falls under this guide, for example:
|
|
12
|
-
|
|
13
|
-
- A single X/Twitter post
|
|
14
|
-
- An X/Twitter thread
|
|
15
|
-
- An X/Twitter long-form post
|
|
16
|
-
- An X/Twitter creator homepage
|
|
17
|
-
- The top N comments from a comment section
|
|
18
|
-
|
|
19
|
-
## Rules
|
|
20
|
-
|
|
21
|
-
- Route these objects through the generic MCP workflow inside this skill.
|
|
22
|
-
- The platform is not limited to the Douyin and Xiaohongshu cases covered by fixed pipelines. If the platform is discoverable in the MCP catalog, try this workflow first.
|
|
23
|
-
- Detect the object first, then use `catalog.search` and `endpoint.describe` to choose the smallest toolchain.
|
|
24
|
-
- Do not jump to browser/CDP only because the platform is not Douyin or Xiaohongshu.
|
|
25
|
-
- Use browser/CDP only when the generic MCP path is unavailable, or when the task explicitly requires page-level interaction that the API cannot satisfy. Explain the reason in the output.
|
|
26
|
-
- The output must satisfy the unified envelope.
|
|
27
|
-
- No card write is required in the first release.
|
|
28
|
-
- Do not fabricate fields only to satisfy schema completeness.
|
|
29
|
-
|
|
30
|
-
## Minimum Deliverable
|
|
31
|
-
|
|
32
|
-
- `object_type`
|
|
33
|
-
- `platform`
|
|
34
|
-
- `input`
|
|
35
|
-
- `normalized`
|
|
36
|
-
- `request_id`
|
|
37
|
-
- `completeness`
|
|
38
|
-
- `missing_fields`
|
|
39
|
-
- `error_reason`
|
|
40
|
-
- `extract_trace`
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# MCP Usage Contract
|
|
2
|
-
|
|
3
|
-
## Scope
|
|
4
|
-
|
|
5
|
-
- This contract applies to every social-media object that does not match a fixed pipeline, not only Douyin and Xiaohongshu.
|
|
6
|
-
- The currently supported platforms include Douyin, Xiaohongshu, Kuaishou, Bilibili, Weibo, TikTok, YouTube, Instagram, Threads, Twitter/X, Reddit, LinkedIn, WeChat Channels, Official Accounts, Toutiao, Xigua, Zhihu, Lemon8, and Pipixia.
|
|
7
|
-
- Typical objects include X/Twitter posts, threads, long-form posts, creator homepages, comment sections, search results, ranking pages, livestream rooms, and product pages.
|
|
8
|
-
- Fixed pipelines are frozen only for Douyin and Xiaohongshu single-work and creator-home cases. All other supported platform and object combinations should use the generic MCP path defined here.
|
|
9
|
-
|
|
10
|
-
## Fixed Inputs
|
|
11
|
-
|
|
12
|
-
- MCP URL: `https://mcp.tikomni.com/mcp`
|
|
13
|
-
- Auth: `Authorization: Bearer <TIKOMNI_API_KEY>`
|
|
14
|
-
- Do not repeat the API key inside tool parameters.
|
|
15
|
-
|
|
16
|
-
## Required Tool Order
|
|
17
|
-
|
|
18
|
-
1. Detect the platform and object type.
|
|
19
|
-
2. Decide whether a fixed pipeline matches.
|
|
20
|
-
3. If a fixed pipeline matches, run the fixed script directly and do not enter the generic MCP path.
|
|
21
|
-
4. If no fixed pipeline matches:
|
|
22
|
-
- `tools/list`
|
|
23
|
-
- `catalog.search`
|
|
24
|
-
- `endpoint.describe`
|
|
25
|
-
- `api.call`
|
|
26
|
-
5. If video text is required:
|
|
27
|
-
- `u2.submit`
|
|
28
|
-
- `u2.query`
|
|
29
|
-
- Enter the U3 fallback path if the task is still `pending` after 60 seconds.
|
|
30
|
-
6. Use browser/CDP only when the generic MCP path is unavailable or clearly insufficient. Do not skip step 4 and jump straight to browser/CDP.
|
|
31
|
-
|
|
32
|
-
## Output Rules
|
|
33
|
-
|
|
34
|
-
- Keep factual fields separate from derived metadata.
|
|
35
|
-
- The result must include `request_id`.
|
|
36
|
-
- The result must include `completeness`.
|
|
37
|
-
- The result must include `missing_fields`.
|
|
38
|
-
- The result must include `error_reason`.
|
|
39
|
-
- The result must include `extract_trace`.
|
|
40
|
-
- If the flow ends in browser/CDP fallback, `extract_trace` must also include the earlier MCP attempts and the fallback reason.
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Minimal MCP HTTP client for social-media-crawl."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import json
|
|
7
|
-
import urllib.request
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
from typing import Any, Dict, Optional
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def _parse_response_body(body: bytes) -> Any:
|
|
13
|
-
text = body.decode("utf-8", errors="replace").strip()
|
|
14
|
-
if not text:
|
|
15
|
-
return {}
|
|
16
|
-
if text.startswith("data:"):
|
|
17
|
-
payloads = []
|
|
18
|
-
for line in text.splitlines():
|
|
19
|
-
if not line.startswith("data:"):
|
|
20
|
-
continue
|
|
21
|
-
payload = line.split("data:", 1)[1].strip()
|
|
22
|
-
if payload:
|
|
23
|
-
payloads.append(payload)
|
|
24
|
-
if payloads:
|
|
25
|
-
return json.loads(payloads[-1])
|
|
26
|
-
return json.loads(text)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@dataclass
|
|
30
|
-
class McpResponse:
|
|
31
|
-
ok: bool
|
|
32
|
-
status_code: int
|
|
33
|
-
data: Any
|
|
34
|
-
session_id: Optional[str]
|
|
35
|
-
error_reason: Optional[str] = None
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class McpHttpClient:
|
|
39
|
-
def __init__(self, *, url: str, api_key: str, timeout_ms: int = 60000) -> None:
|
|
40
|
-
self.url = url.rstrip("/")
|
|
41
|
-
self.api_key = api_key
|
|
42
|
-
self.timeout_ms = timeout_ms
|
|
43
|
-
self.session_id: Optional[str] = None
|
|
44
|
-
self._next_id = 1
|
|
45
|
-
|
|
46
|
-
def _headers(self) -> Dict[str, str]:
|
|
47
|
-
headers = {
|
|
48
|
-
"Authorization": f"Bearer {self.api_key}",
|
|
49
|
-
"Content-Type": "application/json",
|
|
50
|
-
"Accept": "application/json, text/event-stream",
|
|
51
|
-
"User-Agent": "OpenClaw-SocialMediaCrawl/0.1",
|
|
52
|
-
"X-Client-Name": "social-media-crawl",
|
|
53
|
-
"X-Client-Version": "0.1.0",
|
|
54
|
-
}
|
|
55
|
-
if self.session_id:
|
|
56
|
-
headers["mcp-session-id"] = self.session_id
|
|
57
|
-
return headers
|
|
58
|
-
|
|
59
|
-
def _request(self, payload: Dict[str, Any]) -> McpResponse:
|
|
60
|
-
req = urllib.request.Request(
|
|
61
|
-
self.url,
|
|
62
|
-
data=json.dumps(payload).encode("utf-8"),
|
|
63
|
-
headers=self._headers(),
|
|
64
|
-
method="POST",
|
|
65
|
-
)
|
|
66
|
-
try:
|
|
67
|
-
with urllib.request.urlopen(req, timeout=max(self.timeout_ms / 1000.0, 1.0)) as response:
|
|
68
|
-
body = response.read()
|
|
69
|
-
data = _parse_response_body(body)
|
|
70
|
-
session_id = response.headers.get("mcp-session-id") or response.headers.get("Mcp-Session-Id")
|
|
71
|
-
if session_id:
|
|
72
|
-
self.session_id = session_id
|
|
73
|
-
return McpResponse(ok=True, status_code=response.getcode(), data=data, session_id=self.session_id)
|
|
74
|
-
except urllib.error.HTTPError as error:
|
|
75
|
-
body = error.read()
|
|
76
|
-
try:
|
|
77
|
-
data = _parse_response_body(body)
|
|
78
|
-
except Exception:
|
|
79
|
-
data = {"raw": body.decode("utf-8", errors="replace")}
|
|
80
|
-
return McpResponse(
|
|
81
|
-
ok=False,
|
|
82
|
-
status_code=error.code,
|
|
83
|
-
data=data,
|
|
84
|
-
session_id=self.session_id,
|
|
85
|
-
error_reason=f"http_error:{error.code}",
|
|
86
|
-
)
|
|
87
|
-
except Exception as error:
|
|
88
|
-
return McpResponse(
|
|
89
|
-
ok=False,
|
|
90
|
-
status_code=0,
|
|
91
|
-
data={},
|
|
92
|
-
session_id=self.session_id,
|
|
93
|
-
error_reason=f"request_failed:{type(error).__name__}:{error}",
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
def _rpc(self, method: str, params: Optional[Dict[str, Any]] = None, notification: bool = False) -> McpResponse:
|
|
97
|
-
payload: Dict[str, Any] = {"jsonrpc": "2.0", "method": method}
|
|
98
|
-
if not notification:
|
|
99
|
-
payload["id"] = self._next_id
|
|
100
|
-
self._next_id += 1
|
|
101
|
-
if params is not None:
|
|
102
|
-
payload["params"] = params
|
|
103
|
-
return self._request(payload)
|
|
104
|
-
|
|
105
|
-
def initialize(self) -> McpResponse:
|
|
106
|
-
response = self._rpc(
|
|
107
|
-
"initialize",
|
|
108
|
-
{
|
|
109
|
-
"protocolVersion": "2025-03-26",
|
|
110
|
-
"capabilities": {},
|
|
111
|
-
"clientInfo": {"name": "social-media-crawl", "version": "0.1.0"},
|
|
112
|
-
},
|
|
113
|
-
)
|
|
114
|
-
if response.ok:
|
|
115
|
-
self._rpc("notifications/initialized", notification=True)
|
|
116
|
-
return response
|
|
117
|
-
|
|
118
|
-
def tools_list(self) -> McpResponse:
|
|
119
|
-
if not self.session_id:
|
|
120
|
-
init = self.initialize()
|
|
121
|
-
if not init.ok:
|
|
122
|
-
return init
|
|
123
|
-
return self._rpc("tools/list")
|
|
124
|
-
|
|
125
|
-
def tool_call(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> McpResponse:
|
|
126
|
-
if not self.session_id:
|
|
127
|
-
init = self.initialize()
|
|
128
|
-
if not init.ok:
|
|
129
|
-
return init
|
|
130
|
-
return self._rpc("tools/call", {"name": name, "arguments": arguments or {}})
|
|
131
|
-
|
|
132
|
-
def catalog_search(self, query: str) -> McpResponse:
|
|
133
|
-
return self.tool_call("catalog.search", {"query": query})
|
|
134
|
-
|
|
135
|
-
def endpoint_describe(self, method: str, path: str) -> McpResponse:
|
|
136
|
-
return self.tool_call("endpoint.describe", {"method": method, "path": path})
|
|
137
|
-
|
|
138
|
-
def api_call(
|
|
139
|
-
self,
|
|
140
|
-
method: str,
|
|
141
|
-
path: str,
|
|
142
|
-
query: Optional[Dict[str, Any]] = None,
|
|
143
|
-
headers: Optional[Dict[str, Any]] = None,
|
|
144
|
-
body: Optional[Any] = None,
|
|
145
|
-
) -> McpResponse:
|
|
146
|
-
return self.tool_call(
|
|
147
|
-
"api.call",
|
|
148
|
-
{
|
|
149
|
-
"method": method,
|
|
150
|
-
"path": path,
|
|
151
|
-
"query": query or {},
|
|
152
|
-
"headers": headers or {},
|
|
153
|
-
"body": {} if body is None else body,
|
|
154
|
-
},
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
def u2_submit(self, file_url: str) -> McpResponse:
|
|
158
|
-
return self.tool_call("u2.submit", {"file_url": file_url})
|
|
159
|
-
|
|
160
|
-
def u2_query(self, task_id: str) -> McpResponse:
|
|
161
|
-
return self.tool_call("u2.query", {"task_id": task_id})
|