2026-07-31 06:17:25
File upload sounds boring until you've spent an afternoon debugging MultipartException: Failed to parse multipart servlet request in production.
In Spring Boot, file upload involves MultipartFile, MultipartAutoConfiguration, spring.servlet.multipart.* properties, and a silent assumption that you're running on a servlet container. It works, but it carries a lot of hidden machinery.
Solon takes a different path. The core class is UploadedFile, it lives in the web layer with no extra configuration needed, and you have explicit control over when multipart parsing happens and when temporary files get cleaned up.
When a controller method includes an UploadedFile parameter, Solon automatically triggers multipart parsing — no annotation required:
@Controller
public class FileController {
@Post
@Mapping("/upload")
public String upload(UploadedFile file) {
try {
file.transferTo(new File("/storage/uploads/" + file.name));
return "uploaded: " + file.name + " (" + file.contentSize + " bytes)";
} finally {
file.delete(); // always clean up
}
}
}
Key properties you'll use most:
| Property | Description |
|---|---|
file.name |
Full filename including extension |
file.extension |
Extension only (e.g. jpg) |
file.contentType |
MIME type |
file.contentSize |
File size in bytes |
file.content |
Raw InputStream
|
file.contentAsBytes |
byte[] |
file.isEmpty() |
Whether the upload is empty |
One thing worth noting: the framework does not auto-clean temporary files. If you skip delete(), those temp files stay on disk. The try/finally pattern above isn't optional.
Use UploadedFile[] when the client sends multiple files under the same field name (supported since v2.3.8):
@Post
@Mapping("/upload/batch")
public void uploadBatch(UploadedFile[] files) {
for (UploadedFile file : files) {
try {
file.transferTo(new File("/storage/" + file.name));
} finally {
file.delete();
}
}
}
If the form field name differs from your parameter name, use @Param:
@Post
@Mapping("/avatar")
public void setAvatar(@Param("user_avatar") UploadedFile file) {
// handles form field "user_avatar"
}
You can receive both file and text fields in the same request. Parameter injection handles both automatically:
@Post
@Mapping("/upload/with-meta")
public void uploadWithMeta(UploadedFile file, String description, int category) {
try {
// file: the uploaded document
// description, category: plain text form fields
saveFile(file, description, category);
} finally {
file.delete();
}
}
Sometimes a multipart form has only text fields — no file attachment. In that case Solon won't trigger multipart parsing automatically. Use multipart = true on the mapping:
@Post
@Mapping(path = "/submit", multipart = true)
public void submit(String username, int age) {
// multipart form with text fields only
}
You can also access files manually via Context when you need more control:
@Post
@Mapping(path = "/upload/manual", multipart = true)
public void uploadManual(String username, Context ctx) {
UploadedFile file = ctx.file("attachment");
UploadedFile[] extras = ctx.files("extras");
// process...
}
By default, autoMultipart is true — any incoming multipart request will be parsed automatically. On a public-facing service, this means a client can send a large file to any endpoint and trigger parsing overhead.
Tighten this up with a router filter:
Solon.start(App.class, args, app -> {
app.router().filter(-1, (ctx, chain) -> {
// only parse multipart on upload paths
ctx.autoMultipart(ctx.path().startsWith("/upload"));
chain.doFilter(ctx);
});
});
For centralized temp-file cleanup (v2.7.3+), a filter also works well:
@Component
public class MultipartCleanupFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
try {
chain.doFilter(ctx);
} finally {
if (ctx.isMultipartFormData()) {
ctx.filesDelete(); // cleans all uploaded temp files
}
}
}
}
Note: If you use this pattern, don't make your upload handlers async. The filter runs on request completion — async handlers may still be working when cleanup fires.
# app.yml
server:
request:
maxBodySize: 2mb # max request body (default: 2mb)
maxFileSize: 20mb # max single file size
maxHeaderSize: 8kb # max header size
fileSizeThreshold: 512kb # below this: memory; above: temp file (v3.6.0+)
The fileSizeThreshold setting (introduced in v3.6.0) automatically routes small files to memory and large files to disk. Before v3.6.0, you had useTempfile: true for forced temp-file mode — that flag is now deprecated.
Returning files is equally clean. Return DownloadedFile for byte arrays or streams, or just return a java.io.File directly:
@Get
@Mapping("/download/report")
public DownloadedFile downloadReport() {
byte[] pdf = reportService.generatePdf();
return new DownloadedFile("application/pdf", pdf, "report.pdf");
}
@Get
@Mapping("/download/avatar/{userId}")
public File downloadAvatar(@Path String userId) {
return new File("/storage/avatars/" + userId + ".jpg");
}
For more control — caching, inline display, ETags:
@Get
@Mapping("/preview/logo")
public DownloadedFile previewLogo() {
DownloadedFile file = new DownloadedFile(new File("/assets/logo.png"));
file.asAttachment(false); // display inline, don't trigger download
file.cacheControl(3600); // 304 cache for 1 hour
file.eTag("logo-v3");
return file;
}
DownloadedFile also supports HTTP Range, so it works for video streaming and large file resumable downloads without any extra configuration.
Everything above is in solon-web — no separate multipart starter, no auto-configuration class to hunt down:
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-web</artifactId>
</dependency>
Pick any server adapter (JDK HTTP, Jetty, Undertow, Grizzly, smarthttp) — they all support temp-file mode via fileSizeThreshold.
The contrast with Spring is mostly about explicitness:
MultipartFile injected by the servlet layer; MultipartAutoConfiguration registers everything; spring.servlet.multipart.* configures limits; CommonsMultipartResolver optional.UploadedFile parameter → parsing triggered; temp files stay until you call delete(); autoMultipart gives you path-level control; configuration lives in server.request.*.For most projects the difference is minor. Where it matters is when you want to reason clearly about when parsing happens, what lives in memory vs disk, and who is responsible for cleanup. Solon makes those questions explicit instead of hiding them in auto-configuration.
Reference:
2026-07-31 06:13:40
In this series I will be going through how to build an HTTP Server from scratch in C++ using POSIX APIs, and actually have it serve a web page which has CRUD operations to your browser - also it would be capable of serving more than one client at the same time!
Note: I am using C++ 17. And I will be dividing this whole project into multiple parts (maybe 4-5 blogs for now).
Well for starters, you will get to know how TCP sockets work and how to use them to build a server. You will also get to know how HTTP works and how to parse it. You will get to know how to build a web page and how to serve it to the browser. And finally, you will get to understand how complex this architecture gets when trying to serve multiple clients and getting millions of requests! Although I won't be making a server which can handle millions of requests, hopefully you can appreciate the actual server serving you this blog post.
Without further ado, let's start with our first part of this series: TCP Sockets!
Use these includes:
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string>
For the server to accept requests and reply with responses, we need a medium where the server and client can be connected (talk to each other). For that we use sockets. A socket can be thought of as an endpoint of communication between two programs running on a network.
int server_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (server_fd < 0)
{
std::cerr << "error creating the socket" << std::endl;
return -1;
}
The above code creates a socket. AF_INET means we are using IPv4. SOCK_STREAM means we are using TCP. IPPROTO_TCP means we are using the TCP protocol. Alternatively you can just pass 0 as the last argument and the kernel will figure out the protocol based on the socket type (in this case TCP because we used SOCK_STREAM).
The socket function returns an integer file descriptor, which we can use to refer to this socket. Think of a file descriptor as a unique identifier for this socket. If the file descriptor is -1, this means it failed to create a socket.
I recommend adding this snippet after creating a socket:
int opt{1};
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
When you are testing your code you will probably need to start your server multiple times (just like I did). However if you don't do the above step, then you will get an error which tells you that the address is already in use. This happens because the socket gets stuck in TIME_WAIT state after the server is closed. The above code prevents this and lets you reuse the same address immediately after the server is closed.
Now we have the socket, but it is not connected to any address. So we need to bind it to an address. The address is a combination of an IP address and a port number.
struct sockaddr_in server_addr{};
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
std::cerr << "error binding to the socket" << std::endl;
return -1;
}
Here server_addr is a struct which holds the address of the server. sin_addr.s_addr = INADDR_ANY means that the server will listen on all available interfaces. sin_family = AF_INET means we are using IPv4. sin_port = htons(8080) means we are using port 8080. htons() is used to convert the port number to network byte order.
We bind the server using the bind() function, which takes the socket file descriptor, the address of the server, and the size of the address as arguments. Again, if this returns -1, it means it failed to bind the socket.
Now the server is bound to a certain address, meaning it has a home now. But it is still closed and doesn't accept visitors, so we need to make it listen for incoming connections.
if (listen(server_fd, 10) < 0)
{
std::cerr << "error listening on the socket" << std::endl;
return -1;
}
The listen() function tells the kernel that the socket is a passive socket, and it should be used to accept incoming connections. The second argument 10 is the maximum number of pending connections that can be queued. If the queue is full, incoming connections will be rejected. If this returns -1, it means it failed to listen on the socket.
Now that we have our server ready, we can start accepting connections. But before that, I will put all the above code in a function named startServer():
int startServer()
{
int server_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (server_fd < 0)
{
std::cerr << "error creating the socket" << std::endl;
return -1;
}
int opt{1};
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr{};
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
std::cerr << "error binding to the socket" << std::endl;
return -1;
}
if (listen(server_fd, 10) < 0)
{
std::cerr << "error listening on the socket" << std::endl;
return -1;
}
return server_fd;
}
Let's call this function getClient()
int getClient(int server_fd)
{
struct sockaddr_in client_addr{};
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd < 0)
{
std::cerr << "error accepting the client" << std::endl;
return -1;
}
std::cout << "client connected to socket successfully! \n";
return client_fd;
}
The above code now accepts the connection by calling accept() on the server file descriptor, and stores the client address in the client_addr struct. It returns the client file descriptor if successful, and -1 if it fails. client_fd can be understood as the unique identifier for the client's socket.
Since our server is running and accepting clients, next we need to accept their requests and provide appropriate responses. But to do that, we need to receive data from the client and send data to the client.
char buffer[8193]{};
int bytes = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
buffer[bytes] = '\0';
We use the recv() function to receive data from the client. It needs the client file descriptor to identify which client we are talking to, a buffer to store the data, the size of the buffer, and finally a flag set to 0 for default behavior: it blocks until data is received, reads the data from the internal socket buffer, copies it to our buffer, and removes it from the socket buffer (to avoid duplicate reception in the future). It returns the number of bytes received, -1 if it fails, or 0 if the client closes the connection.
// res is of type std::string and contains data to send.
if (send(client_fd, res.c_str(), res.size(), 0) < 0)
{
std::cerr << "Error sending reply: " << res << "\n";
}
We use the send() function to send data to the client. It needs the client file descriptor to identify which client we are talking to, the data to send (as a C-style string - char *), and the size of the data being sent. It returns the number of bytes sent, or -1 if it fails.
Now it's not like a client would only make one request. They can make multiple requests. So we need to handle them continuously. We can do this by using a while loop. I will call the function handleClient(), putting together the recv and send functions inside the loop.
void handleClient(int client_fd)
{
while (true)
{
char buffer[8193]{};
int bytes = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (bytes < 0)
{
std::cerr << "Error receiving message \n";
break;
}
if (bytes == 0)
{
std::cerr << "Client Disconnected \n";
break;
}
buffer[bytes] = '\0';
/**
Parsing request
(Ignore this for now I will explain it in the next blog post)
**/
HttpRequest req = parse_request(buffer);
auto error = isValidRequest(req);
HttpResponse res{};
if (error)
{
res = error.value();
}
else
{
res = route(req);
}
/**
Building Appropriate HTTP Response
(Ignore this for now I will explain it in the next blog post)
**/
std::string http_res = build_response(res);
std::cout << "Received:\n" << buffer << "\n";
std::cout << "Sending:\n" << http_res << "\n";
if (send(client_fd, http_res.c_str(), http_res.size(), 0) < 0)
{
std::cerr << "Error sending reply: " << http_res << "\n";
}
}
close(client_fd);
std::cout << "Client: " << client_fd << " Has Disconnected\n";
}
I know the above code is a lot compared to the code snippet for receiving and sending data. What is actually happening here? Well, I use a while loop so the client can send requests continuously without closing the connection. Remember that recv is a blocking call, meaning it will stop until it gets data from the client or the client closes the connection. So after it receives data and stops blocking, I check if receiving failed (bytes < 0) or if the client left (bytes == 0), in which cases the while loop breaks. Otherwise, I explicitly null-terminate the buffer (buffer[bytes] = '\0') so functions parsing C-strings can read it safely. Finally, I do close(client_fd) to close the connection when the loop breaks.
After successfully receiving the data, I call the function parse_request(buffer) which parses the request and stores it in an HttpRequest struct. I will explain this function in the next blog post. Then I check if the request is valid using isValidRequest(req). If the request is invalid, I send an error response using send_error_response(). Otherwise, I route the request to the appropriate handler using route(req). Finally, I build the HTTP response using build_response(res) and send it to the client using send(client_fd, res.c_str(), res.size(), 0). And that's it.
Receive -> Parse -> Validate -> Build Response -> Send
Now that we have all the required functions, let's put it all together in the main() function.
int main()
{
int server_fd = startServer();
if (server_fd < 0)
{
return 1;
}
std::cout << "Server started on port 8080" << std::endl;
while (true)
{
int client_fd = getClient(server_fd);
if (client_fd < 0)
{
continue;
}
handleClient(client_fd);
}
// The below code would never execute as the while loop runs indefinitely.
close(server_fd);
return 0;
}
We use a while loop so our server runs indefinitely, waiting for clients. (Note: In this initial part, handleClient() processes connections sequentially on a single thread. In future parts of this series, we will make the server handle multiple client connections concurrently using multithreading)
Well, just remove the part of the code commented as "Ignore for now" and change the arguments of send() to buffer and bytes instead of http_res.c_str() and http_res.size() respectively.
Name the file server.cpp.
Now use this command to generate a binary file and to run it in one terminal:
g++ -std=c++17 -Wall server.cpp -o server
./server
Then type nc localhost 8080 in another terminal and then type anything like Hello! in that terminal and press enter. You should see the same text being printed back in your terminal. It will look like the terminal is "stuck" or "waiting" after you type; this is normal as the connection is still open and the server is waiting for more requests. To close the connection, press Ctrl+C.
In the next blog post, I will be explaining Parsing HTTP Requests and Building HTTP Responses.
2026-07-31 06:11:54
کمکم دارم برمیگردم به همون ذهنیتی که همیشه باید میبوده
استمرار و نیت خوب آخرش جواب میده
خیلی چیزا ارزش این همه درگیری ذهنی رو ندارن
برای ناامید شدن زیادی جوونم
زمین خوردن طبیعیه
زخم خوردن طبیعیه
اشتباه کردن طبیعیه
اشتباه رو تکرار کردن هم طبیعیه
ولی موندن همونجا انتخاب خودمونه
بالاخره بیخاصیت که نیستیم
دوست داریم پیشرفت کنیم
بیشتر بدونیم
کارهای مهم و جالب انجام بدیم
مفید باشیم
به درد بخوریم
و خب انسان باشیم
یه سری عادت هایی که ازشون فاصله گرفته بودم رو دوباره دارم میارم توی روتینم
آروم آروم
بدون عجله
بدون اینکه بخوام یه شبه همهچی رو عوض کنم
این چند روز یه پروژه شخصی هم شروع کردم
تقریبا تمام ذهنم درگیرشه
ددلاینش رو برای جمعه هفته بعد گذاشتم
اگه همهچی طبق برنامه پیش بره سورسش رو منتشر میکنم
امروز یه مقاله هم خوندم
این بخشش جالب بود
راجع به اینکه چرا هنوز بلاگها مهمن
الان هر کسی میتونه از ایآی بپرسه چجوری فلان کار رو انجام بدم و چند ثانیه بعد جوابش رو بگیره
پس فکر نکنم ارزش این لاگها توی آموزش دادن باشه
چیزی که هنوز هیچ مدلی نمیتونه از طرف من بنویسه مسیر خودمه
اینکه امروز روی چه چیزایی که از نظر ایآی احمقانهست گیر کردم
چیا رو خراب کردم
چی یاد گرفتم
یا حتی چرا یه کتاب یا یه گفتوگو برام موندگار شد
شاید برای همین بیشتر از اینکه بخوام بگم چطوری انجامش بدی
دوست دارم بنویسم من چطوری انجامش دادم
و حتی وقتی همهچی تیره به نظر میرسه
یه گوشه از ذهنم هنوز داره آروم میگه
ادامه بده
2026-07-31 06:08:46
Claude Code is fluent in your repository and blind to your content. It can refactor the component that renders your blog, but ask it to publish the post that component displays and it has nowhere to look. The Model Context Protocol (MCP) closes that gap. It gives Claude Code a set of tools it can call against your CMS directly, so reading and writing content happens in the same conversation as the code.
This guide connects Claude Code to a Cosmic bucket. With the hosted endpoint it takes about five minutes and requires no install.
MCP is an open protocol for exposing tools to AI assistants. The Cosmic MCP server implements it and exposes 18 tools across four areas:
Once connected, "publish the MCP draft and generate a hero image for it" resolves to real tool calls against your bucket. No browser tab, no copy-paste, no manual export.
There are two ways to connect:
https://mcp.cosmicjs.com/v1/buckets/{your-bucket-slug} and authenticate with your bucket keys. Nothing to install.@cosmicjs/mcp npm package locally via npx. Useful for offline work, or when you want the MCP process running inside your own dev environment.You need three things:
The hosted path needs no local runtime at all. Node is only required if you choose the self-hosted stdio option, since that runs through npx.
A recommendation before you paste anything: start with the read key only. Cosmic issues separate read and write keys per bucket, so you can give Claude Code full visibility into your content while making it structurally incapable of changing it. Add the write key once you trust the setup. The read-only vs full access section below covers exactly what changes.
Claude Code picks up project-scoped MCP servers from a .mcp.json file at the root of your repository. Create it with:
{
"mcpServers": {
"cosmic": {
"url": "https://mcp.cosmicjs.com/v1/buckets/your-bucket-slug",
"headers": {
"Authorization": "Bearer your-read-key:your-write-key"
}
}
}
}
Replace your-bucket-slug, your-read-key, and your-write-key with the values from Step 1. The endpoint supports the streamable-HTTP MCP transport.
Cosmic packs both keys into a single bearer token, separated by a colon. The write key is the part after the colon:
# Read-only access
Authorization: Bearer rk_abc123def456
# Full access (read + write)
Authorization: Bearer rk_abc123def456:wk_zyx987wvu654
Omit the colon and the write key for read-only access. If your client cannot send a colon-packed token, you can pass the write key out-of-band using the X-Cosmic-Write-Key header instead.
One housekeeping note: .mcp.json now contains live credentials, so add it to .gitignore before your next commit.
The same mcpServers block works for Claude Desktop and Cursor. The MCP server docs list the exact config file paths for each client, including ~/Library/Application Support/Claude/claude_desktop_config.json on macOS and .cursor/mcp.json for Cursor.
If you would rather run the server yourself, the @cosmicjs/mcp package ships a stdio binary. Point Claude Code at npx:
{
"mcpServers": {
"cosmic": {
"command": "npx",
"args": ["@cosmicjs/mcp"],
"env": {
"COSMIC_BUCKET_SLUG": "your-bucket-slug",
"COSMIC_READ_KEY": "your-read-key",
"COSMIC_WRITE_KEY": "your-write-key"
}
}
}
}
The stdio binary reads credentials from environment variables:
COSMIC_BUCKET_SLUG (required): your Cosmic bucket slugCOSMIC_READ_KEY (required): bucket read key for read operationsCOSMIC_WRITE_KEY (optional): bucket write key for write operationsLeave COSMIC_WRITE_KEY out entirely for a read-only server. You can also install it globally with npm install -g @cosmicjs/mcp instead of resolving it through npx each time.
Restart Claude Code and run:
/mcp
You should see cosmic listed with its tools. Then confirm it can actually reach your bucket by asking for something only your bucket knows:
List all object types in my Cosmic bucket
Claude Code should call cosmic_types_list and return your real content models. If it returns your object types, the connection is live and correctly authenticated.
cosmic_objects_list: list or search objects, filtered by type, status, and locale, with paginationcosmic_objects_get: fetch a single object by ID or slug, with optional metafield, depth, and locale paramscosmic_objects_create: create a new object with title, slug, status, and metafields (write key required)cosmic_objects_update: update an existing object's title, slug, status, or metafield values (write key required)cosmic_objects_delete: permanently delete an object by ID (write key required)cosmic_media_list: list media files, optionally scoped to a foldercosmic_media_get: fetch metadata and the imgix URL for a single filecosmic_media_upload: upload from a URL or base64 payload into the media library (write key required)cosmic_media_delete: delete a media file (write key required)cosmic_types_list: list every object type in the bucketcosmic_types_get: fetch the full schema for one object type, including metafields, options, and helper textcosmic_types_create: create a new object type with a metafield schema (write key required)cosmic_types_update: update a type's schema or metafield definitions (write key required)cosmic_types_delete: delete an object type and all its objects (write key required)cosmic_ai_generate_text: generate text with optional context pulled from existing objects in your bucketcosmic_ai_generate_image: generate an image and store it in the media library (write key required)cosmic_ai_generate_video: generate video with Google Veo and store it in the media library (write key required)cosmic_ai_generate_audio: generate narration via OpenAI TTS, 13 voices available, stored in the media library (write key required)The two tools worth calling out for agent work are cosmic_types_list and cosmic_types_get. An agent that reads your schema before writing produces valid metafields on the first attempt instead of guessing key names and failing.
This is the part to get right before you point an agent at a production bucket.
With a read-only token, every write tool is blocked with a clear error message and read tools work as normal. Specifically, the blocked set is every *_create, *_update, and *_delete tool, plus all four AI generation tools, since each of those writes generated assets into your media library.
So a read-only setup still lets Claude Code explore your content models, read every object, and reason about your content while it writes application code. It just cannot mutate anything. That is a good default for a first session against real data.
With the server connected, these are all single prompts:
List all blog posts in my Cosmic bucket
Create a new blog post titled "Getting Started with MCP" with the content
"This is an introduction to the Model Context Protocol..."
Update the blog post with ID "abc123" to change its status to published
Show me all images in the "blog-images" folder
Create a new object type called "Products" with fields for name, price,
description, and image
Generate audio narration of "Welcome to Cosmic CMS" using the "nova" voice
and upload it to my media library
The schema management case is the one developers tend to underestimate. Modeling content is usually a dashboard task. Through MCP it becomes something you can do from the same prompt where you are scaffolding the components that will consume it.
The hosted endpoint exposes a second, smaller scope at https://mcp.cosmicjs.com/v1/agent for the agent signup flow. It lets an AI agent provision a brand new Cosmic project and bucket on behalf of someone who does not have an account, without leaving the MCP transport. It exposes three tools:
cosmic_agent_signup (no auth): creates an unclaimed project and bucket tied to a human_email. Returns the agent_key, read_key, write_key, and a claim_url. Cosmic emails the human a 6-digit OTP.cosmic_agent_verify (requires agent_key): submits the OTP, lifts restricted-mode limits, and enables AI generation.cosmic_agent_status (requires agent_key): checks claim status, remaining limits, and recovers the bucket keys.New buckets start in restricted mode: no AI credits, a maximum of 50 objects, and a 5 MB media cap. Unclaimed projects are hard-deleted after 14 days.
The bucket-scoped tools listed earlier are not available on the agent endpoint, and the agent tools are not available on the bucket endpoint. A single conversation often uses both: the agent signs the human up, captures the returned bucket keys, then switches to the bucket scope to start creating content.
Cosmic offers two things that sound similar and do different jobs:
Use both. Agent Skills helps Claude Code write code like this:
import { createBucketClient } from '@cosmicjs/sdk';
const cosmic = createBucketClient({
bucketSlug: 'your-bucket-slug',
readKey: 'your-read-key',
});
const { objects: posts } = await cosmic.objects
.find({ type: 'blog-posts' })
.props(['title', 'slug', 'metadata'])
.depth(1);
The MCP server then lets the same session manage the content that code renders. One tool writes the app, the other operates the data behind it.
Four habits worth adopting:
cosmic_objects_delete, cosmic_media_delete, and especially cosmic_types_delete are permanent, and deleting an object type takes all of its objects with it. Never let an agent call these speculatively.The server does not appear in /mcp. Confirm .mcp.json is valid JSON at the repository root and restart Claude Code. Some Claude Code versions want the transport named explicitly, so if a hosted config still will not connect, try adding "type": "http" alongside url.
npx cannot find the package. The package name is @cosmicjs/mcp, scoped, including the @. Verify Node is installed and on your PATH.
Write tools return an error but reads work. Your bearer token is missing the write key. Check that the header is Bearer READ_KEY:WRITE_KEY with a colon and no spaces, or send the write key via X-Cosmic-Write-Key.
404 from the hosted endpoint. The bucket slug in the URL is wrong. Copy it again from Settings -> API Access, since the slug is not always identical to your project's display name.
Tools connect but return nothing. Confirm you are pointed at the bucket you think you are. Ask Claude Code to run cosmic_types_list and compare the result against the dashboard.
Start with the hosted endpoint and a read-only token. Ask Claude Code to list your object types, then ask it to summarize the content in your bucket. Once that works, add the write key and let it draft something. The full tool reference and per-client config paths live in the MCP server documentation.
Try it yourself. Cosmic is an AI-powered headless CMS with a REST API, a TypeScript SDK, and a hosted MCP server. Create a free account and connect Claude Code in about five minutes. Evaluating Cosmic for a team? Book a call with Tony.
Originally published on the Cosmic blog.
2026-07-31 06:08:33
Most Solidity scanners are high-recall, low-precision. They flag 40 things, 38 are noise, and after the third report you stop reading them — so the one real bug ships. Precision, not recall, is what makes a security tool actually get used.
I've been building OpenClaw, a heuristic Solidity scanner with the opposite bar: silence on sound code. To pressure-test it, I pointed it at six codebases that top firms have already audited — Yearn, Sablier, Ajna, Liquity, and a couple of smaller protocols — and hand-verified every single flag.
The result: 14 HIGH/CRITICAL candidates across the six. Every one was a false positive.
That sounds like a failure. It's the whole point — and each FP is a lesson in the exact traps that fool most scanners. Here they are.
The scanner flagged balanceOf(address(this)) used in accounting as a donation / share-inflation attack. But the contract was a fork of OpenZeppelin's PaymentSplitter: shares are fixed at deployment, and a "donation" is exactly the input that gets split proportionally among those fixed shares. There's no share to mint, no share price to manipulate, no first-depositor. balance + totalReleased is the canonical, correct accounting.
Lesson: balanceOf(this) in accounting is only a donation risk when shares are minted against it. In a fixed-share pull-payment splitter, it's intended behavior.
Ajna's pools were flagged as "unprotected initialize()". But there it was, first line:
if (isPoolInitialized) revert AlreadyInitialized();
A one-time-init guard. Plenty of non-OpenZeppelin protocols protect initialize with a boolean flag + revert instead of the initializer modifier. A detector that only knows the OZ modifier misses it and screams.
Lesson: an initializer is protected if it has any one-time guard — the OZ modifier, a boolean flag that reverts, factory/clone init, or _disableInitializers().
poolBalanceDetails(), flashLoan(), simulateRedemption() — all flagged for reading a balance after an external call. Read-only reentrancy (the Curve/Balancer class that paid $100k+ bounties) is only exploitable if an external protocol consumes the function as a price oracle. A view utility in a Multicall helper, a flash loan's repayment check, a simulate* function — none of those are oracles.
Lesson: read-only reentrancy needs an oracle consumer. simulate / preview / *Details / multicall / flashLoan aren't oracles → not exploitable.
Flagged CRITICAL: "rate can be updated." But the rate was managed and bounded — a rateManager role, a maxRateChangePerUpdate cap, a rateUpdateInterval, and events on every change. A trusted, bounded, time-gated exchange rate is intended design, not an exploit. Even a malicious manager can only nudge it by the cap per interval.
Lesson: a role-updatable value bounded by max-change + interval is a managed parameter, not a vulnerability (at most a centralization note).
Flagged for not handling fee-on-transfer tokens. But Liquity's BOLD uses a vetted, fixed set of collateral (WETH/wstETH-class), not arbitrary ERC-20s. "Doesn't support FoT" is a deliberate decision for a protocol with curated collateral.
Lesson: FoT-not-supported is only a bug if the protocol accepts arbitrary tokens. With vetted collateral, it's intended.
Every one of these is a place a naive scanner shouts "CRITICAL" and a good auditor quietly says "no." The value of a low false-positive rate isn't that the tool finds less — it's that when it (or I) stay silent, the silence means something. A tool that cries wolf 38 times out of 40 trains you to ignore the 39th. The 39th is the real one.
Each of these five FP classes is now a permanent fix in the detector, not a patch — so the next PaymentSplitter, the next flag-guarded initialize, the next view utility, doesn't fire. That's how you drive false positives toward zero without going blind to the real thing.
OpenClaw is calibrated against the codebases everyone treats as a gold standard — OpenZeppelin, Solady, Solmate, Uniswap v2/v3/v4, Permit2, Morpho Blue, PRBMath. 608 source files, 14 total flags, 0 across the entire OpenZeppelin library. MIT, pure Python, runs as a GitHub Action that comments on every PR:
pipx run --spec git+https://github.com/juan23z/openclaw-audit openclaw-audit <repo> --out ./report
Honest about what it is not: it's heuristic, not formal verification. It catches classes of bugs (access control, vault math, reentrancy shape, oracle staleness, upgradeability) — not your protocol's bespoke economic logic bug, the one where two functions interact in a way nobody drew on the whiteboard. That still needs a human. But the silence is honest.
If you're shipping to mainnet and want a second set of eyes — hand-verified findings, zero false-positive spam, a plain-English report in 48h — I do fast pre-mainnet reviews. Or just run the free scanner and keep the signal.
I'd genuinely like feedback on the false-positive classes above — which ones have bitten you with Slither/others?
2026-07-31 06:08:15
Manab Protim Hazarika didn't take the usual route into software.
Growing up in Titabor, a small town in the Jorhat district of Assam, he completed his Higher Secondary in Science before enrolling in an ITI (Industrial Training Institute) program in the Electrician trade. It was a hands-on, practical path with nothing to do with code. Yet, at sixteen in 2017, he began teaching himself programming on the side—without a computer science degree or a formal curriculum. He relied solely on curiosity and a willingness to break things.
That unique combination—practical trade training paired with self-taught programming—ultimately shaped how he approaches every project today: build the real thing first, and understand the theory as you need it.
Becoming a Developer
Manab Protim started with Android and PHP, focusing on tools he could successfully run on affordable hardware by relying on documentation, trial, and error. He later picked up Java, then MySQL, and eventually Flutter when he needed to develop faster across multiple platforms.
There was no single "bootcamp moment" or massive break. Instead, there were years of small projects—most of which were never seen by anyone else—long before he shipped anything that mattered to a broader audience.
Becoming an Entrepreneur
Eventually, building software for himself transitioned into building it for others. He founded Protim.co.in, a software development studio, to formalize his work, offering Android, iOS, Flutter, and PHP web development for clients across India. Over time, the agency's portfolio grew to encompass projects in food and hospitality, education, retail, and even a website for a national-level political party.
However, the projects Manab Protim cares about the most are the ones nobody hired him to build:
Titabor Kitchen: An Android app connecting home kitchens and local restaurants with residents in his hometown, addressing the complete absence of organized food delivery.
Numaligarh Shop: A digital commerce app providing local retailers in Numaligarh with a platform to reach customers online.
These weren't driven by grand business strategies; they were simply decisions born out of realizing, "This problem exists two streets from my house, and I know how to fix it."
Orzyy
His most recent and widest-reaching product is Orzyy—a QR-code-based vehicle safety app. It allows vehicle owners to be contacted by strangers for issues like wrong parking, accidents, or deliveries without ever exposing their phone numbers. The app is now live globally on Google Play, a remarkable milestone for a platform built to solve a localized problem he noticed on Indian roads.
What He Believes
Manab Protim firmly believes that you don't need a computer science degree, a metro city address, or major funding to build something people actually use. Instead, he believes all you need is a real problem in front of you and the stubbornness to keep learning until you can solve it. Everything he has built—from earning an ITI electrician certificate to launching a globally available application—has stemmed from applying that exact same approach over and over again.
What's Next
Today, Manab Protim continues to build both client software through Protim.co.in and his own independent products like Orzyy. His focus remains on practical, privacy-conscious tools designed for problems that mainstream tech tends to overlook—especially in smaller Indian towns that aren't usually prioritized by developers.
He is always open to discussing Flutter, Android, bootstrapping in smaller markets, or building for underserved communities. You can connect with him at manab.protim.co.in.