Search and inspect public X posts through the Xquik REST API.
Search public X posts with the Xquik API
Get one public X post by ID with the Xquik API
#:package [email protected]
#:package [email protected]
#:property PublishAot=false
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Nodes;
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(consoleLogOptions =>
{
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
await builder.Build().RunAsync();
public record XquikApiResult(int StatusCode, bool IsSuccess, JsonNode? Body);
[McpServerToolType]
public static class XquikTools
{
private const string ApiBaseUrl = "https://xquik.com";
private static readonly HttpClient Http = new()
{
Timeout = TimeSpan.FromSeconds(30)
};
[McpServerTool, Description("Search public X posts with the Xquik API")]
public static XquikApiResult SearchTweets(
[Description("Search query, X status URL, tweet ID, or account date window")] string query,
[Description("Sort order: Latest or Top")] string queryType = "Latest",
[Description("Maximum tweets to return, from 1 to 200")] int limit = 20,
[Description("Pagination cursor from a previous response")] string? cursor = null,
[Description("Only return posts after this ISO 8601 timestamp")] string? sinceTime = null,
[Description("Only return posts before this ISO 8601 timestamp")] string? untilTime = null)
{
if (string.IsNullOrWhiteSpace(query))
{
throw new ArgumentException("query is required");
}
var safeLimit = Math.Clamp(limit, 1, 200);
var safeQueryType = string.Equals(queryType, "Top", StringComparison.OrdinalIgnoreCase)
? "Top"
: "Latest";
var parameters = new List<KeyValuePair<string, string>>
{
new("q", query),
new("queryType", safeQueryType),
new("limit", safeLimit.ToString())
};
AddOptional(parameters, "cursor", cursor);
AddOptional(parameters, "sinceTime", sinceTime);
AddOptional(parameters, "untilTime", untilTime);
return SendGet("/api/v1/x/tweets/search", parameters);
}
[McpServerTool, Description("Get one public X post by ID with the Xquik API")]
public static XquikApiResult GetTweet(
[Description("X post ID")] string tweetId)
{
if (string.IsNullOrWhiteSpace(tweetId))
{
throw new ArgumentException("tweetId is required");
}
return SendGet($"/api/v1/x/tweets/{Uri.EscapeDataString(tweetId)}", []);
}
private static void AddOptional(List<KeyValuePair<string, string>> parameters, string name, string? value)
{
if (!string.IsNullOrWhiteSpace(value))
{
parameters.Add(new KeyValuePair<string, string>(name, value));
}
}
private static XquikApiResult SendGet(string path, IEnumerable<KeyValuePair<string, string>> parameters)
{
var apiKey = Environment.GetEnvironmentVariable("XQUIK_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
{
throw new InvalidOperationException("Set XQUIK_API_KEY before calling Xquik tools.");
}
var queryString = string.Join("&", parameters.Select(
parameter => $"{Uri.EscapeDataString(parameter.Key)}={Uri.EscapeDataString(parameter.Value)}"));
var uri = string.IsNullOrEmpty(queryString)
? new Uri($"{ApiBaseUrl}{path}")
: new Uri($"{ApiBaseUrl}{path}?{queryString}");
using var request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using var response = Http.Send(request);
var bodyText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
var body = TryParseJson(bodyText);
return new XquikApiResult((int)response.StatusCode, response.IsSuccessStatusCode, body);
}
private static JsonNode? TryParseJson(string body)
{
if (string.IsNullOrWhiteSpace(body))
{
return null;
}
try
{
return JsonNode.Parse(body);
}
catch (JsonException)
{
return JsonValue.Create(body);
}
}
}
Put it in your project, in a .mcp-servers/ directory. Copy the code above, or download it from /servers/xquik/xquik.cs. Commit it: the file is the whole server.
The first build restores the packages. It takes longer than the startup timeout of most clients, so run it by hand before you go on:
dotnet build .mcp-servers/xquik.cs -v q
This adds no bin or obj directory to your project. A file-based app is not a project, so the SDK caches the build under your temp directory instead. Your .gitignore needs no new line.
Claude Code and Codex read different files, in different formats. Pick yours, add the snippet to your project root, and commit it. Everyone who clones the project then gets the server. Don't paste one client's format into the other's file.
Claude Code — .mcp.json
Note: This example also covers Claude Desktop, Cursor, and Windsurf. Visual Studio and VS Code use servers instead of mcpServers.
{
"mcpServers": {
"xquik": {
"type": "stdio",
"command": "dotnet",
"args": ["run", "./.mcp-servers/xquik.cs", "-v", "q"],
"env": {
"XQUIK_API_KEY": "${XQUIK_API_KEY}"
}
}
}
}
Because this file is committed, it names the variable and never holds the value. Claude Code expands ${...} from your environment when it reads the file.
claude mcp add xquik --scope project -- dotnet run ./.mcp-servers/xquik.cs -v q writes the same entry for you. Full details: /install/claude-code.md.
Codex — .codex/config.toml
[mcp_servers.xquik]
command = "dotnet"
args = [
"run",
"./.mcp-servers/xquik.cs",
"-v",
"q",
]
env_vars = ["XQUIK_API_KEY"]
startup_timeout_sec = 60
Codex needs the project trusted. It ignores a project .codex/config.toml unless your personal ~/.codex/config.toml holds [projects.'<absolute path>'] with trust_level = "trusted" — and there is no error message when it doesn't. On Windows use the plain C:\Users\... path, never the extended \\?\C:\... form. Don't commit that entry. See /install/codex.md.
Codex does not expand ${...}. Its env table takes literal values, so env_vars names the variable and Codex forwards the value from its own environment.
Both paths are relative to the project, so they work for everyone who clones it, on every operating system. -v q is required in both: standard output carries the JSON-RPC stream, and without it the build output can reach that stream and break the connection.
Restart your client in the project directory, then check that xquik shows as connected. Claude Code asks you to approve a project server the first time it reads .mcp.json. Codex reads its configuration at startup only, so restart it and open a new chat.
Required configuration for this server
XQUIK_API_KEY
Xquik
Maintained by the AnyMCP community