get_posts() from parent category only using a shortcode

If you don’t want to include posts in child categories, use the category__in parameter:

$args = array(
  'category__in' => [ get_cat_ID( $category_name ) ], // use category__in
  //'category_name' => $category_name,                // and not this.
  'post_type' => 'menu',
  'numberposts' => -1,
  'post_status' => 'publish'
);

And although the above would give you what you wanted, the following might help you..

function fetch_teds_menu_items($atts)
{
  $atts = shortcode_atts(array(
    'category_name' => ''
  ), $atts);

  if ( ! $category = get_term_by( 'name', $atts['category_name'], 'category' ) ) {
    return '';
  }

  $args = array(
    'category__in' => [ $category->term_id ],
    'post_type' => 'menu',
    'numberposts' => -1,
    'post_status' => 'publish'
  );

  $meta_data = get_term_meta($category->term_id, 'category_featured_image', TRUE);
  // $category_image = wp_get_attachment_url($meta_data);

  $subcategories = get_categories(
    array(
      'parent' => $category->term_id
    )
  );

  $output="<section id="" . $category->slug . '">';
  ...
  return $output;
}

I.e. I use get_term_by() to get the full category object by its name (not slug, but can be slug).

PS: The category parameter also include child categories — 'category' => $subcategory->cat_ID,.