Inspiration

My inspiration to build this project was my love for art and wanting to be able to make art of different type with ease. I wanted a place where all your creative needs are meet so you don't have to know how to use different softwares to get different creative things done. I also wanted to build a app that was easy to use for anyone even beginners.

What it does

For now creative hub is an application where you can draw and download the pieces you draw locally, We have not added a backend so all data is stored locally in your browser and auto saved.

How we built it

This software was built using the leptos rust frontend frameworks. And hosted with github pages. WebMCP was added through a JS file exporting it's function to install and register the 25tools in our array

export function installWebMcp(dispatch) {
  const callJson = (name, argsJson) => dispatch(name, argsJson || "{}");
  const call = (name, args) => JSON.parse(callJson(name, JSON.stringify(args || {})));
let idleTimer = null;
  const bump = () => {
    if (idleTimer) clearTimeout(idleTimer);
    idleTimer = setTimeout(() => {
      try { dispatch("__force_end_batch", "{}"); } catch (_) {}
    }, IDLE_CLOSE_MS);
  };

  const mc =
    (typeof document !== "undefined" && document.modelContext) ||
    (typeof navigator !== "undefined" && navigator.modelContext) ||
    null;

  let registered = false;
  if (mc && typeof mc.registerTool === "function") {
    for (const t of TOOLS) {
      const annotations = t.destructive
        ? { destructiveHint: true, readOnlyHint: false }
        : t.readOnly
        ? { readOnlyHint: true }
        : { readOnlyHint: false };
      try {
        mc.registerTool({
          name: t.name,
          description: t.description,
          inputSchema: t.inputSchema,
          annotations,
          async execute(args) {
            bump();
            // `args` may arrive as a parsed object or as a JSON string
            // depending on the browser's modelContext implementation.
            let obj = parseArgs(args);
            if (t.name === "place_image") {
              obj = await normalizeImageArgs(obj);
              if (obj && obj.__error) {
                return {
                  content: [
                    { type: "text", text: JSON.stringify({ success: false, error: obj.__error }) },
                  ],
                };
              }
            }
            const text = callJson(t.name, JSON.stringify(obj || {}));
            return { content: [{ type: "text", text }] };
          },
        });
      } catch (err) {
        console.warn("[webmcp] registerTool failed for", t.name, err);
      }
    }
    registered = true;
  }

  // Always available — dev panel + future extension shim.
  if (typeof window !== "undefined") {
    window.__webmcp = {
      available: registered,
      tools: TOOLS.map((t) => t.name),
      schemas: TOOLS,
      call, // (name, obj) -> parsed result (sync; place_image needs a data: URL + explicit size here)
      // async variant: runs the same image-normalisation step as the real
      // modelContext execute() path, so place_image accepts a remote URL.
      callAsync: async (name, args) => {
        bump();
        let obj = args || {};
        if (name === "place_image") {
          obj = await normalizeImageArgs(obj);
          if (obj && obj.__error) return { success: false, error: obj.__error };
        }
        return JSON.parse(callJson(name, JSON.stringify(obj)));
      },
      dispatch: (name, argsJson) => {
        bump();
        return callJson(name, argsJson);
      },
    };
  }

  return registered;
}

The dispatch is a rust function that checks to match the tool to an ArtAction


#[wasm_bindgen(module = "/src/webmcp/webmcp.js")]
extern "C" {
    /// Registers the tools with `document.modelContext` (when present) and
    /// always publishes `window.__webmcp`. Returns whether a real
    /// `modelContext` was found. `dispatch` is `(toolName, argsJson) -> resultJson`.
    #[wasm_bindgen(js_name = installWebMcp)]
    fn install_webmcp(dispatch: &JsValue) -> bool;

    /// Dev-panel entry point: `(toolName, argsJson) -> resultJson`, routed
    /// through `window.__webmcp` so it exercises the real JS <-> WASM path.
    #[wasm_bindgen(js_name = webmcpCall)]
    fn webmcp_call_js(name: &str, args_json: &str) -> String;

    /// `{ "available": bool, "tools": [name, ...] }` as JSON — for the dev panel.
    #[wasm_bindgen(js_name = webmcpStatus)]
    fn webmcp_status_js() -> String;
}

thread_local! {
    static REGISTERED: Cell<bool> = const { Cell::new(false) };
}

/// One-time tool registration. Safe to call on every mount of the drawing view
/// — only the first call does anything.
pub fn init() {
    if REGISTERED.with(|r| {
        let already = r.get();
        r.set(true);
        already
    }) {
        return;
    }

    let dispatch = Closure::<dyn Fn(String, String) -> String>::new(
        |name: String, args_json: String| -> String {
            let parsed: serde_json::Value =
                serde_json::from_str(&args_json).unwrap_or(serde_json::Value::Null);
            commands::dispatch(&name, parsed)
        },
    );

    let _found = install_webmcp(dispatch.as_ref().unchecked_ref());

    // The closure must outlive this function — it is the tool executor for the
    // life of the page. Registration happens once, so this leak is bounded.
    dispatch.forget();
}          

Challenges we ran into

Some of the challenges we ran through were architectural challenges on how we setup the canvas and was using one canvas for world operations and drawing, this proved a problem when flood fill was to be implemented so we switched to 3 canvas for the world (Pan & Zoom), Drawing (Lines, Fills), and preview. We also had a bit of a technical issue setting up webmcp cause we couldn't pass the enum used to build the art engine to the JavaScript code, but we instead made a dispatcher that solves that issue.

Accomplishments that we're proud of

So far I am very proud of where the project has reached currently especially the fact that it is now looking like a product with the WebMCP addition. But I am proud to be at a point where I can show the product.

What we learned

We learned a lot from this project from architecture to capabilities of canvas in image processing, And we are going to learn a whole lot more by the time we start building out other aspects of this project.

What's next for Creative Hub

The next for creative hub would be to setup a backend and start working on the image editing/processing aspect of things.

Built With

  • leptos
  • rust
  • wasm
  • webmcp
Share this project:

Updates