Building an MCP for Meteor

Hi all,

I’ve been thinking, how can I help improving the adoption of Meteorjs as the go-to development framework. I’ve been using MeteorJs from v1, but left the framework last year to explore other frameworks (Nextjs). The newer frameworks are sweet, but nothing can compare the experience I had with Meteor.

So, introducing meteor-mcp, an MCP server I am building (with the help of Claude Code), specifically designed to serve tools for MeteorJs. Suitable to be used by vibe-coders. Hopefully by introducing this, more will adopt Meteorjs as their framework of choice when building (vibe-coding) web apps and systems.

I’ve hosted the server myself, but you can host it yourself. Everything is in the github repo. Give it a try.

4 Likes

Awesome! We were just talking about this here:

1 Like

“Use your tools to check MeteorJS documentation and show me a sample code for a validated method and a sample for a publication and subscription in React. Also add a comment for the Meteor version used.”

I use, like other millions of developers, the Context7 mcp.

Response :

Based on the MeteorJS documentation, here are sample code patterns for validated methods and publication/subscription in React.

Meteor Version: 3.4 (Current version used in your project)


1. Validated Method Sample

Using simpl-schema for Validation (Meteor 3.x)

// ===========================================
// File: imports/api/collections/posts/server/methods.js
// Meteor Version: 3.4
// ===========================================

import SimpleSchema from 'simpl-schema'
import { Meteor } from 'meteor/meteor'
import { Posts } from './collection'

// Method with inline validation
Meteor.methods({
  'posts.create': async function ({ content, tags, visibility }) {
    // Validate input using SimpleSchema
    new SimpleSchema({
      content: { type: String, max: 5000 },
      tags: { type: Array, optional: true },
      'tags.

Using mdg:validated-method Package (Alternative)

// ===========================================
// File: imports/api/collections/posts/methods.js
// Meteor Version: 3.4
// ===========================================

import { Meteor } from 'meteor/meteor'
import SimpleSchema from 'simpl-schema'
import { ValidatedMethod } from 'meteor/mdg:validated-method'
import { Posts } from './collection'

export const createPost = new ValidatedMethod({
  name: 'posts.create',
  
  validate: new SimpleSchema({
    content: { type: String, max: 5000 },
    tags: { type: Array, optional: true },
    'tags.

2. Publication Sample

// ===========================================
// File: imports/api/collections/posts/server/publications.js
// Meteor Version: 3.4
// ===========================================

import { Meteor } from 'meteor/meteor'
import SimpleSchema from 'simpl-schema'
import { Posts } from './collection'

Meteor.publish('posts.user', function publishUserPosts({ userId, limit, skip }) {
  // Validate arguments
  new SimpleSchema({
    userId: { type: String },
    limit: { type: Number, min: 1, max: 100, defaultValue: 20 },
    skip: { type: Number, min: 0, defaultValue: 0 }
  }).validate({ userId, limit, skip })

  // Security: filter by visibility
  const selector = {
    userId,
    $or: [
      { visibility: 'public' },
      { userId: this.userId }
    ]
  }

  return Posts.find(selector, {
    fields: { userId: 1, content: 1, tags: 1, visibility: 1, stats: 1, createdAt: 1 },
    limit, skip, sort: { createdAt: -1 }
  })
})

3. React Subscription Sample

// ===========================================
// File: imports/ui/pages/UserPosts.jsx
// Meteor Version: 3.4
// ===========================================

import React from 'react'
import { useSubscribe, useFind } from 'meteor/react-meteor-data'
import { Posts } from '../../api/collections/posts/collection'
import Loading from '../components/Loading'
import PostCard from '../components/PostCard'

function UserPosts({ userId }) {
  // useSubscribe returns a function that indicates loading state
  const isLoading = useSubscribe('posts.user', { userId, limit: 20, skip: 0 })

  // useFind fetches data reactively from Minimongo
  const posts = useFind(
    () => Posts.find({ userId }, { sort: { createdAt: -1 } }),
    [userId]
  )

  if (isLoading()) return <Loading />

  return (
    <div className="user-posts">
      <h2>User Posts</h2>
      {posts.length === 0 ? <p>No posts found.</p> : posts.map(post => <PostCard key={post._id} post={post} />)}
    </div>
  )
}

export default UserPosts

Calling Methods with Meteor.callAsync()

// ===========================================
// File: imports/ui/components/CreatePostForm.jsx
// Meteor Version: 3.4
// ===========================================

import React, { useState } from 'react'
import { Meteor } from 'meteor/meteor'
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField'

function CreatePostForm() {
  const [content, setContent] = useState('')
  const [submitting, setSubmitting] = useState(false)

  const handleSubmit = async (e) => {
    e.preventDefault()
    setSubmitting(true)
    try {
      const postId = await Meteor.callAsync('posts.create', { content, visibility: 'public' })
      console.log('Post created:', postId)
      setContent('')
    } catch (err) {
      console.error('Error:', err.reason)
    } finally {
      setSubmitting(false)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <TextField value={content} onChange={(e) => setContent(e.target.value)} multiline rows={4} />
      <Button type="submit" disabled={submitting || !content.trim()}>
        {submitting ? 'Creating...' : 'Create Post'}
      </Button>
    </form>
  )
}

Key Differences: Meteor 2.x vs 3.x

Feature Meteor 2.x Meteor 3.x
Method calls Meteor.call() with callbacks Meteor.callAsync() with async/await
Database ops Collection.insert() Collection.insertAsync()
React hooks useTracker() useSubscribe(), useFind()
Loading state Boolean Function isLoading()
  visibility: { 
    type: String, 
    allowedValues: ['public', 'connections', 'private'] 
  }
}).validate({ content, tags, visibility })

// Check authentication
if (!this.userId) {
  throw new Meteor.Error(401, 'Not logged in', 
    'You must be logged in to create a post.')
}

// Perform database operation (async in Meteor 3.x)
const postId = await Posts.insertAsync({
  userId: this.userId,
  content,
  tags: tags || [],
  visibility: visibility || 'public',
  stats: { likesCount: 0, commentsCount: 0, sharesCount: 0, viewsCount: 0 },
  createdAt: new Date(),
  updatedAt: new Date()
})

return postId

}
})


### Using `DISCOURSE_PLACEHOLDER_3` Package (Alternative)

DISCOURSE_PLACEHOLDER_4


---

## 2. Publication Sample

DISCOURSE_PLACEHOLDER_5


---

## 3. React Subscription Sample

DISCOURSE_PLACEHOLDER_6


### Calling Methods with `DISCOURSE_PLACEHOLDER_7`

DISCOURSE_PLACEHOLDER_8


---

## Key Differences: Meteor 2.x vs 3.x

|Feature|Meteor 2.x|Meteor 3.x|
| --- | --- | --- |
|Method calls|`DISCOURSE_PLACEHOLDER_9` with callbacks|`DISCOURSE_PLACEHOLDER_10` with async/await|
|Database ops|`DISCOURSE_PLACEHOLDER_11`|`DISCOURSE_PLACEHOLDER_12`|
|React hooks|`DISCOURSE_PLACEHOLDER_13`|`DISCOURSE_PLACEHOLDER_14`, `DISCOURSE_PLACEHOLDER_15`|
|Loading state|Boolean|Function `DISCOURSE_PLACEHOLDER_16`|: { type: String },
    visibility: { 
      type: String, 
      allowedValues: ['public', 'connections', 'private'],
      defaultValue: 'public'
    }
  }).validator(),

  async run({ content, tags, visibility }) {
    if (!this.userId) {
      throw new Meteor.Error('posts.create.unauthorized', 'Must be logged in.')
    }
    return await Posts.insertAsync({ userId: this.userId, content, tags, visibility, createdAt: new Date() })
  }
})

2. Publication Sample

DISCOURSE_PLACEHOLDER_5

3. React Subscription Sample

DISCOURSE_PLACEHOLDER_6

Calling Methods with DISCOURSE_PLACEHOLDER_7

DISCOURSE_PLACEHOLDER_8

Key Differences: Meteor 2.x vs 3.x

Feature Meteor 2.x Meteor 3.x
Method calls DISCOURSE_PLACEHOLDER_9 with callbacks DISCOURSE_PLACEHOLDER_10 with async/await
Database ops DISCOURSE_PLACEHOLDER_11 DISCOURSE_PLACEHOLDER_12
React hooks DISCOURSE_PLACEHOLDER_13 DISCOURSE_PLACEHOLDER_14, DISCOURSE_PLACEHOLDER_15
Loading state Boolean Function DISCOURSE_PLACEHOLDER_16
  visibility: { 
    type: String, 
    allowedValues: ['public', 'connections', 'private'] 
  }
}).validate({ content, tags, visibility })

// Check authentication
if (!this.userId) {
  throw new Meteor.Error(401, 'Not logged in', 
    'You must be logged in to create a post.')
}

// Perform database operation (async in Meteor 3.x)
const postId = await Posts.insertAsync({
  userId: this.userId,
  content,
  tags: tags || [],
  visibility: visibility || 'public',
  stats: { likesCount: 0, commentsCount: 0, sharesCount: 0, viewsCount: 0 },
  createdAt: new Date(),
  updatedAt: new Date()
})

return postId

}
})


### Using `DISCOURSE_PLACEHOLDER_3` Package (Alternative)

DISCOURSE_PLACEHOLDER_4


---

## 2. Publication Sample

DISCOURSE_PLACEHOLDER_5


---

## 3. React Subscription Sample

DISCOURSE_PLACEHOLDER_6


### Calling Methods with `DISCOURSE_PLACEHOLDER_7`

DISCOURSE_PLACEHOLDER_8


---

## Key Differences: Meteor 2.x vs 3.x

|Feature|Meteor 2.x|Meteor 3.x|
| --- | --- | --- |
|Method calls|`DISCOURSE_PLACEHOLDER_9` with callbacks|`DISCOURSE_PLACEHOLDER_10` with async/await|
|Database ops|`DISCOURSE_PLACEHOLDER_11`|`DISCOURSE_PLACEHOLDER_12`|
|React hooks|`DISCOURSE_PLACEHOLDER_13`|`DISCOURSE_PLACEHOLDER_14`, `DISCOURSE_PLACEHOLDER_15`|
|Loading state|Boolean|Function `DISCOURSE_PLACEHOLDER_16`|